Why NestJS, and what its structure is for
You can build an API in Node with about six lines of Express. NestJS asks for considerably more structure than that, and this lesson is the argument for why — because if you do not understand what the structure buys you, it will feel like ceremony and you will fight it.
What a small Express API looks like
const express = require("express");
const app = express();
app.get("/products", async (req, res) => {
const products = await db.query("SELECT * FROM products");
res.json(products);
});
app.listen(3001);
Six lines, and it works. For a webhook receiver or a health check, this is the right amount of machinery.
What it looks like at three months
app.post("/orders", async (req, res) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).json({ error: "Unauthorized" });
let user;
try {
user = jwt.verify(token, process.env.JWT_SECRET);
} catch {
return res.status(401).json({ error: "Invalid token" });
}
if (!req.body.items || !Array.isArray(req.body.items)) {
return res.status(400).json({ error: "items must be an array" });
}
for (const item of req.body.items) {
if (!item.productId || typeof item.quantity !== "number") {
return res.status(400).json({ error: "Invalid item" });
}
}
try {
const order = await db.transaction(async (tx) => {
// sixty lines of stock checks, pricing and inserts
});
res.status(201).json(order);
} catch (error) {
console.error(error);
res.status(500).json({ error: "Something went wrong" });
}
});
Nothing here is unreasonable in isolation. Together:
- The token check is copied into every protected route. Twenty routes, twenty copies, and one of them is subtly different.
- Validation is written by hand and re-written per endpoint.
- Business logic sits inside the HTTP handler, so it cannot be called from anywhere else or tested without simulating a request.
- The database is reached directly, so tests need a real one.
- Error handling is repeated at the bottom of every route.
That file grows to four hundred lines and everybody is slightly afraid of it. The experienced Express answer is to impose structure yourself — routers, middleware, service modules, a validation library. That works, and every team does it differently.
What NestJS does
NestJS is that structure, decided for you:
@Controller("orders")
export class OrdersController {
constructor(private readonly ordersService: OrdersService) {}
@Post()
@UseGuards(JwtAuthGuard)
async create(
@CurrentUser() user: User,
@Body() dto: CreateOrderDto,
): Promise<OrderResponse> {
return this.ordersService.create(user.id, dto);
}
}
Compare it against the list above.
Authentication is a guard. Written once, applied with one line, and impossible to write slightly differently on route seventeen.
Validation comes from the DTO. CreateOrderDto declares the shape and a
pipe enforces it before your code runs. A malformed request never reaches this
method.
Business logic is in a service. Testable with no HTTP involved, and callable from a controller, a scheduled job, or a queue consumer.
Errors are handled centrally. Throw NotFoundException anywhere and a
filter turns it into a 404 with a consistent body.
The controller is four lines and describes what the endpoint is, not how every mechanism works.
The price
Being honest, because this matters when you are deciding on a real project:
More files. A feature is a module, a controller, a service and a couple of DTOs. Five files where Express needed one.
Decorators and dependency injection. Two concepts to learn before anything works, and both are unusual if you have only written JavaScript.
A steeper start. An afternoon before your first endpoint, against ten minutes with Express.
It can be too much. For a three-endpoint internal tool, this is not worth it.
The trade is real: NestJS costs more at the beginning and less at month six. On a small script the beginning is all there is; on a shop with orders, stock, payments and an admin area, month six arrives quickly.
Where the structure comes from
NestJS is modelled closely on Angular — modules, decorators, dependency injection, providers. If you have used Angular it will feel familiar. If not, the concepts are worth learning on their own merits, and they transfer to Spring and .NET, which are built the same way.
The practical reason it matters: a NestJS project looks the same everywhere.
Any NestJS developer can find the order logic in your codebase in thirty
seconds, because it is in orders.service.ts. That is worth a great deal on a
team, and worth something even alone when you return after six months.
The pieces
Everything in this module fills in one of these:
| Piece | Job |
|---|---|
| Module | groups a feature and declares what it needs and provides |
| Controller | maps HTTP requests to methods |
| Service | the business logic |
| Provider | anything injectable — usually a service |
| DTO | the shape of a request body, with validation rules |
| Pipe | transforms and validates input before the handler |
| Guard | decides whether a request may proceed |
| Interceptor | wraps a handler — logging, shaping responses |
| Filter | turns thrown exceptions into HTTP responses |
The mental model for a request:
request → middleware → guard → interceptor → pipe → handler
↓
response ← filter (if thrown) ← interceptor ← ────────┘
Knowing that order answers a lot of "why did my validation not run" questions. A guard rejecting a request means the pipe never ran, so a 401 takes precedence over a 400.
When not to use it
Use something lighter when:
- The API is a handful of endpoints that will not grow
- It is a single-purpose function — a webhook, a cron job
- The team knows Express well and the project is short-lived
Use NestJS when:
- The domain has real rules — this shop qualifies
- Several people will work on it
- It will be maintained for years
- You want testing, validation and auth solved rather than assembled
This course uses it because the Kirana Store has genuine business logic: stock that can run out mid-checkout, prices that change, orders that move through states. That is exactly the situation the structure pays for.
Check your work
The main argument for NestJS: it provides one consistent structure for authentication, validation, business logic and error handling, so those are written once rather than repeated per route.
The cost: more files, two new concepts (decorators and dependency injection), and a slower start.
Why business logic goes in a service, not a controller: so it can be tested without HTTP and called from somewhere other than a request — a job, a queue, a script.
Request order: middleware, guard, interceptor, pipe, handler — then the response travels back through interceptors, and through a filter if something was thrown.
Why a 401 takes precedence over a 400: the guard runs before the validation pipe, so an unauthenticated request is rejected before its body is examined.
When not to use it: a few endpoints, a single-purpose service, or a short-lived project.
Practice
No new code yet — this is the lesson to think about rather than type.
- Look at the Express
POST /ordersabove. List every concern mixed into that one function. - For each, say which NestJS piece would own it.
- Write down what would have to change to call that order logic from a scheduled job instead of a request. Then say why the service version needs no change.
- Explain in one sentence why validation in the controller is worse than validation in a DTO.
- Name two projects you would not use NestJS for, and why.
- Open
apps/api/srcfrom module 1. Findmain.ts,app.module.tsandapp.controller.tsand say what each appears to do before we cover it.
Next: modules, which is where every NestJS feature starts.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship