Nest vocabulary: the words before the wiring
- Explain what a controller, service, provider and module each do, and put a business rule in the service
- Explain that decorators run once at startup, and write a route with @Controller, @Get, @Param, @Body
- Choose the HTTP verb for an operation and explain why path and query values arrive as strings
- Read constructor(private readonly x: T) as an injected field and explain why provider fields are shared
- Tell a guard (yes/no gate) from an interceptor (wraps the call) and reach the request via switchToHttp()
Read
A whole Nest app in three files
FlagA Nest backend is a program that waits for HTTP requests and answers them. A request is
just a message: GET /users/42 means "give me the user with id 42". (This track is written
against Nest 12, the current major.)
Everything else is Nest giving you a fixed place to put your code. Three files, three jobs:
// users.service.ts — the WORK. Plain class. Knows nothing about HTTP.
@Injectable()
export class UsersService {
findOne(id: string) {
return { id, name: "Ada" };
}
}
// users.controller.ts — the DOOR. Maps a URL to a method call.
@Controller("users") // every route in this class starts with /users
export class UsersController {
constructor(private readonly users: UsersService) {}
@Get(":id") // GET /users/42
findOne(@Param("id") id: string) {
return this.users.findOne(id); // hand the work to the service
}
}
// users.module.ts — the LIST. Tells Nest these two classes exist.
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Say each one out loud once and the rest of Nest gets much smaller:
- A controller is the door: it receives requests and returns a response. Its job is translation — read the URL and body, call something, hand back the result.
- A service is the work: business rules, database calls, calculations. No HTTP words in it.
- A module is the list: "here are the classes in this feature." Nest reads it at startup to know what exists.
- Provider is the general word for anything the module can hand to someone else. A service is the most common kind of provider. So: every service is a provider; not every provider is a service (a config object or a database connection can be one too).
Why the split? Because the work outlives the door. The same UsersService can be called by an
HTTP controller today and by a scheduled job or a queue consumer tomorrow, and it's testable
without pretending to be a web server.
constructor(private readonly x) — the line that confuses everyone
FlagHalf of "I don't understand Nest" is really "I don't understand this one TypeScript line".
export class UsersController {
constructor(private readonly users: UsersService) {}
findAll() {
return this.users.findAll();
}
}
Take it apart:
constructor— the method that runs when the object is created. Its parameters are what the class needs in order to exist.private readonly users: UsersService— a parameter property. This one line does three things that you'd otherwise write out longhand:
// Longhand: exactly the same class.
export class UsersController {
private readonly users: UsersService; // 1. declare a field on the object
constructor(users: UsersService) { // 2. take it as an argument
this.users = users; // 3. store it on the object
}
}
private— "only code inside this class may touchthis.users." It's a compile-time rule; TypeScript erases it, so nothing stops a caller at runtime. Use it to say intent.readonly— you can't reassignthis.userslater. Same deal: compile-time only.this— the object the method was called on. InsidefindAll(),thisis that controller instance, sothis.usersis the service that was handed to it. A plain local variable would vanish when the constructor returned; a field onthissurvives for the life of the object.- A
methodis just a function that lives on a class and can usethis.findAll()above is a method; the arrow function you pass to.map()is not.
Why "declared, not constructed" matters
Notice what the constructor does not do: it never says new UsersService(). It only
declares "I need a UsersService". Something else — Nest's injector — decides which object
to pass in.
That's dependency injection, and its whole payoff shows up in tests:
// No Nest, no HTTP, no database.
const controller = new UsersController({ findAll: () => [{ id: "1" }] } as UsersService);
expect(controller.findAll()).toHaveLength(1);
If the controller had built its own service with new, you couldn't swap it — you'd be forced
to boot the real thing (and its database) just to test one if.
The cast of characters around your handler
FlagYour controller method is called the handler. Nest lets you put reusable code around every handler instead of copying it into each one. Those pieces have names, and the names are the whole problem — each is just a class with one method.
| Name | One-line job | The method you write |
|---|---|---|
| Middleware | Anything framework-level, before Nest is involved | use(req, res, next) |
| Guard | May this request continue? Yes/no | canActivate(context) → boolean | Promise<boolean> | Observable<boolean> |
| Interceptor | Wrap the call — do something before and after | intercept(context, next) |
| Pipe | Check/convert one argument | transform(value, metadata) |
| Exception filter | Turn a thrown error into a response for the current transport | catch(error, host) |
They run in a fixed order around the handler:
middleware → guard → interceptor(before) → pipe → HANDLER → interceptor(after)
↘ (anything thrown) ↘ filter
The filter isn't a step everyone passes through — it's the branch taken when something throws. A request that succeeds never touches it.
Middleware, and the "consumer"
Middleware is the Express-style (req, res, next) function — it runs before Nest's own layers
and knows nothing about which handler will eventually run. You attach it by implementing
configure on the module, and the object you're handed is the MiddlewareConsumer. Odd
name, boring meaning: it's the little builder API for saying which routes get this middleware.
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestIdMiddleware).forRoutes("*"); // "consume" this middleware, for every route
}
}
Guard, and what "context" means
A guard answers one question: should this request be allowed to proceed? Return true and it
continues; return false and Nest responds 403. It may also return a promise or an Observable
of that boolean, which is what lets a guard hit a database or verify a token before deciding.
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const expected = process.env.API_KEY;
if (!expected) return false; // no key configured -> fail closed, not open
const request = context.switchToHttp().getRequest();
return request.headers["x-api-key"] === expected;
}
}
That first line matters: without it, an unset API_KEY makes undefined === undefined true and
the guard waves everyone through. A comparison-based check should always confirm it has
something to compare against.
The context is Nest's envelope around "the thing currently being handled". It's not
HTTP-specific, because the same guard could be protecting a WebSocket message or a queue
message — so before you can touch request, you have to say which world you're in:
context.switchToHttp() (or switchToWs(), switchToRpc()). It also carries
getHandler() / getClass(), which is how a guard reads decorators you put on the route, like
@Roles("admin").
The other three, briefly
- An interceptor sees both sides of the handler, so it's where timing, logging, caching and
"wrap every response in
{ data }" live. - A pipe runs on a single argument on its way in —
"42"→42, or reject the body as a 400 when it doesn't match the DTO. - An exception filter is the last stop for a failed request: whatever was thrown anywhere in that chain becomes a status code and a response body here (JSON by default — a filter can render or redirect instead).
You don't need to be able to write these yet. You need to be able to read a codebase and say "that's a guard, so it's about permission" — the units after this one build each one properly.
Quiz
You're adding "a customer can't have more than 3 open orders". Which file?
FlagWhat's the relationship between a "service" and a "provider"?
FlagWhen does the @Get(":id") decorator actually run?
@Controller("users") on the class, @Post(":id/orders") on the method. What URL does that answer?
A client wants to change only an order's note field, leaving everything else alone. Which verb?
@Param("id") id: string gets /users/42. What's in id, and why does the type say string?
What does constructor(private readonly users: UsersService) {} do, beyond taking an argument?
A service sets this.currentUser = user at the top of a method, awaits a DB call, then reads this.currentUser. What's wrong?
In plain terms, what separates a guard from an interceptor?
FlagWhy does a guard write context.switchToHttp().getRequest() instead of just taking req?
Fill in the blank
Mark this class as something the injector may create and hand out.
Flag@() export class UsersService { findAll() { return []; } }
Answer GET /users/42: the class prefix, the verb decorator with a route parameter, and the decorator that reads it.
@("users") export class UsersController { @(":id") findOne(@("id") id: string) { return { id }; } }
Create an order from the request's JSON body.
Flag@() create(@() dto: CreateOrderDto) { return this.orders.create(dto); }
Give this controller the service in one line — declared, never constructed.
Flagexport class UsersController { constructor( readonly users: UsersService) {} findAll() { return .users.findAll(); } }
Register the controller and the service for this feature.
Flag@Module({ : [UsersController], : [UsersService], }) export class UsersModule {}
Reach the HTTP request from inside a guard (this one admits read requests only).
FlagcanActivate(context: ExecutionContext): boolean { const request = context.().getRequest(); return request.method === "GET"; }