Same job, more structure
If you've built an Express API, you already understand routes, middleware, and request handling. NestJS wraps the exact same ideas in a more opinionated structure, using decorators instead of manually wiring everything together.
Controllers are just grouped route handlers
import { Controller, Get, Post, Body } from "@nestjs/common";
import { PostsService } from "./posts.service";
@Controller("posts")
export class PostsController {
constructor(private readonly postsService: PostsService) {}
@Get()
findAll() {
return this.postsService.findAll();
}
@Post()
create(@Body() body: { title: string; content: string }) {
return this.postsService.create(body);
}
}
Services hold the actual logic
Instead of writing database calls directly inside a route handler like you might in Express, NestJS pushes that logic into an injectable service - which makes it easier to test in isolation and reuse elsewhere.
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { Post } from "./post.schema";
@Injectable()
export class PostsService {
constructor(@InjectModel(Post.name) private postModel: Model<Post>) {}
findAll() {
return this.postModel.find().lean();
}
create(data: { title: string; content: string }) {
return this.postModel.create(data);
}
}
Modules tie related pieces together
A module just groups a controller with the services and dependencies it needs - think of it as the NestJS equivalent of an Express router file plus its own little dependency container.
import { Module } from "@nestjs/common";
import { PostsController } from "./posts.controller";
import { PostsService } from "./posts.service";
@Module({
controllers: [PostsController],
providers: [PostsService],
})
export class PostsModule {}
Why bother with the extra structure
On a small project, this probably feels like overhead. On a larger one with several developers, the enforced separation between controllers, services, and modules keeps the codebase predictable in a way that a loose collection of Express route files eventually stops being.
Comments
Loading comments...
Related Articles
Taming Slow MongoDB Aggregation Pipelines in Production
An aggregation pipeline that returns instantly on your 500-document local dataset can crawl once it hits a few million real documents. Here's how to actually find and fix the stage that's costing you.
Read ArticleBuilding a Sliding-Window Rate Limiter for Node.js APIs with Redis
Fixed-window rate limiting looks fine in a demo and then lets through double the traffic right at the window boundary. Here's the sliding-window-counter approach that fixes that, built directly on Redis.
Read ArticleMulti-Tenant Data Isolation: Shared Schema vs Separate Databases
Every multi-tenant SaaS eventually has to answer one question honestly: how do you stop one customer from ever seeing another customer's data? Here's how the three common isolation strategies actually compare in practice.
Read Article