DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

NestJS Complete Guide: Enterprise Node.js APIs (2026)

NestJS is the most downloaded TypeScript backend framework on npm. Where Express gives you routing primitives and leaves architecture to you, NestJS prescribes a module system, dependency injection container, and decorator-based API that scales across large teams.

Installation

npm install -g @nestjs/cli
nest new my-api
cd my-api
npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Controllers

import { Controller, Get, Post, Patch, Delete, Param, Body, Query, ParseUUIDPipe } from '@nestjs/common'
import { PostsService } from './posts.service'
import { CreatePostDto } from './dto/create-post.dto'

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Get()
  findAll(@Query('page') page = 1, @Query('limit') limit = 20) {
    return this.postsService.findAll({ page: +page, limit: +limit })
  }

  @Get(':id')
  findOne(@Param('id', ParseUUIDPipe) id: string) {
    return this.postsService.findOne(id)
  }

  @Post()
  create(@Body() dto: CreatePostDto) {
    return this.postsService.create(dto)
  }

  @Delete(':id')
  remove(@Param('id', ParseUUIDPipe) id: string) {
    return this.postsService.remove(id)
  }
}
Enter fullscreen mode Exit fullscreen mode

Services

import { Injectable, NotFoundException } from '@nestjs/common'
import { PrismaService } from '../prisma/prisma.service'

@Injectable()
export class PostsService {
  constructor(private readonly prisma: PrismaService) {}

  async findOne(id: string) {
    const post = await this.prisma.post.findUnique({ where: { id } })
    if (!post) throw new NotFoundException(`Post ${id} not found`)
    return post
  }

  async create(dto: CreatePostDto) {
    const slug = dto.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')
    return this.prisma.post.create({ data: { ...dto, slug } })
  }

  async remove(id: string) {
    await this.findOne(id)
    await this.prisma.post.delete({ where: { id } })
  }
}
Enter fullscreen mode Exit fullscreen mode

Validation with DTOs

npm install class-validator class-transformer
Enter fullscreen mode Exit fullscreen mode
import { IsString, IsBoolean, IsOptional, MinLength, MaxLength } from 'class-validator'

export class CreatePostDto {
  @IsString()
  @MinLength(3)
  @MaxLength(200)
  title: string

  @IsString()
  @MinLength(10)
  content: string

  @IsOptional()
  @IsBoolean()
  published?: boolean
}
Enter fullscreen mode Exit fullscreen mode

Enable globally in main.ts:

app.useGlobalPipes(new ValidationPipe({
  whitelist: true,           // strip extra properties
  forbidNonWhitelisted: true,
  transform: true            // auto-convert types
}))
Enter fullscreen mode Exit fullscreen mode

Modules

@Module({
  controllers: [PostsController],
  providers: [PostsService],
  exports: [PostsService]
})
export class PostsModule {}
Enter fullscreen mode Exit fullscreen mode

JWT Authentication

// jwt.strategy.ts
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private usersService: UsersService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET
    })
  }

  async validate(payload: { sub: string; email: string }) {
    const user = await this.usersService.findOne(payload.sub)
    if (!user) throw new UnauthorizedException()
    return user
  }
}

// Protect routes
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Request() req) {
  return req.user
}
Enter fullscreen mode Exit fullscreen mode

Custom Decorator

export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user
)

// Usage
@Get('profile')
@UseGuards(JwtAuthGuard)
getProfile(@CurrentUser() user: User) {
  return user
}
Enter fullscreen mode Exit fullscreen mode

Testing

describe('PostsService', () => {
  let service: PostsService

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        PostsService,
        {
          provide: PrismaService,
          useValue: {
            post: {
              findUnique: jest.fn(),
              create: jest.fn(),
              delete: jest.fn()
            }
          }
        }
      ]
    }).compile()

    service = module.get<PostsService>(PostsService)
  })

  it('throws NotFoundException for missing post', async () => {
    jest.spyOn(service['prisma'].post, 'findUnique').mockResolvedValue(null)
    await expect(service.findOne('bad-id')).rejects.toThrow(NotFoundException)
  })
})
Enter fullscreen mode Exit fullscreen mode

Swagger

npm install @nestjs/swagger
Enter fullscreen mode Exit fullscreen mode
// main.ts
const config = new DocumentBuilder()
  .setTitle('My API')
  .setVersion('1.0')
  .addBearerAuth()
  .build()

SwaggerModule.setup('api/docs', app, SwaggerModule.createDocument(app, config))
Enter fullscreen mode Exit fullscreen mode

NestJS vs Fastify vs Express

NestJS Fastify Express
Architecture Opinionated Minimal Minimal
DI container Built-in Manual Manual
Validation class-validator pipes JSON Schema External
Swagger Auto-generated Manual Manual
Learning curve High Medium Low
Best for Large teams High-throughput Simple apps

Full article at stacknotice.com/blog/nestjs-complete-guide-2026

Top comments (0)