typescript
57 lines · 7 steps
Building a CRUD service in NestJS
A NestJS provider wraps a TypeORM repository to create, read, update, and delete users while guarding against duplicates and missing rows.
Explained by
highlit
1import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
2import { InjectRepository } from '@nestjs/typeorm';
3import { Repository, DeepPartial } from 'typeorm';
4import { User } from './entities/user.entity';
5import { CreateUserDto } from './dto/create-user.dto';
6import { UpdateUserDto } from './dto/update-user.dto';
7
8@Injectable()
9export class UsersService {
10 constructor(
11 @InjectRepository(User)
12 private readonly usersRepository: Repository<User>,
13 ) {}
14
15 async create(dto: CreateUserDto): Promise<User> {
16 const existing = await this.usersRepository.findOne({
17 where: { email: dto.email },
18 });
19
20 if (existing) {
21 throw new ConflictException(`Email ${dto.email} is already registered`);
22 }
23
24 const user = this.usersRepository.create(dto as DeepPartial<User>);
25 return this.usersRepository.save(user);
26 }
27
28 findAll(): Promise<User[]> {
29 return this.usersRepository.find({
30 order: { createdAt: 'DESC' },
31 });
32 }
33
34 async findOne(id: string): Promise<User> {
35 const user = await this.usersRepository.findOne({ where: { id } });
36
37 if (!user) {
38 throw new NotFoundException(`User ${id} not found`);
39 }
40
41 return user;
42 }
43
44 async update(id: string, dto: UpdateUserDto): Promise<User> {
45 const user = await this.findOne(id);
46 Object.assign(user, dto);
47 return this.usersRepository.save(user);
48 }
49
50 async remove(id: string): Promise<void> {
51 const result = await this.usersRepository.delete(id);
52
53 if (result.affected === 0) {
54 throw new NotFoundException(`User ${id} not found`);
55 }
56 }
57}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Injecting a repository keeps data access declarative and testable while the service focuses on business rules.
- 2Translating data conditions into NestJS HTTP exceptions gives callers meaningful status codes for free.
- 3Reusing findOne inside update centralizes the not-found check so every read path enforces the same guarantee.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-crud-service-in-nestjs-explained-typescript-b4a3/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.