The questions that actually come up, with answers drawn from the nineteen lessons before this one. Most of them are really one question — do you understand where each piece of the framework sits, and why — so the answers say why rather than reciting an API.
What is NestJS, and what is it not?
A structure for a server-side TypeScript application, plus a dependency injection container. Underneath, by default, it is Express — so it is not a runtime, not faster than Express, and gives you no capability Express lacks.
What it gives you is an answer to "where does this code go", and a defined request pipeline where cross-cutting concerns are declared once instead of being a middleware array each route opts into correctly. Worth being able to say when it is not worth it: an application with three endpoints.
Why is my provider not found?
Because providers are private to their module. Two declarations are needed — the owning module exports it, the consuming module imports the owner:
@Module({
imports: [TypeOrmModule.forFeature([Project, Quote, ServiceCategory])],
controllers: [ProjectsController],
providers: [ProjectsService],
exports: [ProjectsService],
})
export class ProjectsModule {}Read the error rather than guessing: ? marks the unresolvable parameter, the index
says which constructor argument, and "available in the X context" is the important half —
the class exists, it is just not visible from there.
How does Nest know what to inject?
emitDecoratorMetadata in tsconfig.json. TypeScript types are erased, so
the compiler emits the constructor's parameter types as runtime values beside the class, and Nest
looks each one up. The class is the token.
The follow-up: so why can you not inject an interface? Because an interface leaves
nothing behind at runtime. Use a named token and @Inject.
Guards, interceptors, middleware, pipes — which and why?
The order they run in settles it:
middleware -> guards -> interceptors -> pipes -> handler -> interceptors -> filtersMiddleware is outermost and the only stage that runs even when a guard rejects
the request. It gets the raw request and response and no ExecutionContext.
Guards answer yes or no, and nothing else.
Interceptors wrap the handler, so they are the only stage that sees both the request and the outcome.
Pipes transform or validate one parameter, immediately before the handler.
Four questions choose between them: does it need to run for rejected requests (middleware), is the answer yes/no (guard), does it need the outcome (interceptor), does it transform a value (pipe)?
Why is my request id on 200s but not 401s?
Because it was assigned by an interceptor. A request rejected by a guard never reaches an interceptor, so the requests you most want to correlate — the failures — are exactly the ones with no id. Assign it in middleware, which runs before the guards:
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer.apply(RequestIdMiddleware).forRoutes('*')
}
}Why does my validation not run?
Because class-validator decorators are metadata and something has to read them. That
something is the global ValidationPipe:
app.useGlobalPipes(
new TrimPipe(),
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: false,
},
}),
)Without it the DTOs are documentation and every field arrives unchecked — an application that looks validated and is not, with nothing about it looking wrong.
Know what the options do. whitelist strips undecorated properties, making the DTO a
real allowlist. forbidNonWhitelisted rejects instead of stripping.
transform builds an actual DTO instance and is what makes @Type()
work.
Why is @UseGuards(RolesGuard, JwtAuthGuard) broken?
Guards run in the order listed, and RolesGuard reads the user
JwtAuthGuard attaches. Reversed, it sees no user and 403s everything — which reads like
a permissions misconfiguration and sends you looking at roles.
How does @Roles() actually work?
Two halves. SetMetadata attaches the list to the route and enforces nothing:
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles)A guard reads it back with Reflector:
const required = this.reflector.getAllAndOverride<UserRole[] | undefined>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
])getAllAndOverride rather than get, so a method-level decorator beats a
class-level one. The follow-up worth having ready: what if there is no decorator? No
requirement — return true. Returning false would 403 every unannotated
route.
Should an authorization check be a guard?
Only if it can be decided from the token and the route. "Only contractors may quote" can. "Only the hired contractor may mark this project complete" cannot — it depends on a row — so it belongs in the service, which is loading that row anyway.
A strong answer mentions the status code: a resource you do not own returns 404, not 403, because a 403 confirms the id exists and on a guessable identifier that is a slow enumeration of the whole table.
What happens to an exception on its way out?
An exception filter catches it. Nest has a global one already, which is why a service can throw
NotFoundException and the client gets a 404 with no try/catch
anywhere.
You write your own when something that is not an HttpException gets thrown, because
the built-in filter makes all of those a bare 500:
if (code === PG_UNIQUE_VIOLATION) {
return {
status: HttpStatus.CONFLICT,
body: { message: 'That has already been done.', error: 'Conflict' },
}
}Two things to say about writing one. Keep message's shape — a string normally, an
array for validation errors — or you have made a breaking change. And never put a stack trace or a
raw error message in the body; log it against the request id instead.
APP_FILTER or app.useGlobalFilters()?
providers: [
{ provide: APP_FILTER, useClass: AllExceptionsFilter },
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
],The token puts it in the DI container, so it can inject — and a test that imports
AppModule picks it up automatically. Bound on the app instance it must be repeated in
every test bootstrap, and the day somebody forgets, the suite exercises a pipeline production does
not have.
How do you test a service without a database?
Supply its collaborators as useValue mocks. The trap is the token:
{
provide: getDataSourceToken(),
useValue: {
getRepository: () => ({ findOne: async () => storedUser }),
},
},@InjectDataSource() asks for a named token, not the class — so overriding
DataSource compiles, runs, and leaves the service holding the real connection. Same for
getRepositoryToken(User).
What is @Global() for, and when should you use it?
It makes a module's exports available without being imported. Legitimate for a genuinely ambient
concern — this application uses it once, for auth, because every feature module needs
JwtAuthGuard and the alternative is five imports that exist only to satisfy the
container.
The cost is what makes this a good question: a module's imports are a statement of its dependencies, and a global provider is a dependency that appears in none of them. Two or three global modules and the graph has stopped being readable.
forRoot versus forFeature?
forRoot configures something once, globally — one database connection. It is called
in the root module. forFeature registers a slice for one module and opens no second
connection; it lists the entities that module touches, which doubles as documentation.
The Async variants exist for when the options need something injected, which is
almost always configuration.
Why must synchronize stay false?
It derives the schema from the entities with no migration to write, silently drops columns it believes are gone, and leaves no record of how the schema got where it is. Migrations are that record. Related: never edit an applied migration — Postgres has already run it, so the edit only changes what new databases get and the two diverge forever.
Are providers singletons?
Yes, by default: constructed once and shared by every request, which is why per-request overhead
is small. Scope.REQUEST and Scope.TRANSIENT exist, and request scope is
contagious — anything injecting a request-scoped provider becomes request-scoped, propagating up
the graph until a large part of the application is rebuilt per request.
The good follow-up is what to do instead: attach per-request state to the request in a guard and
read it with a parameter decorator, which is what @CurrentUser() does.
Why does a relative import end in .js?
Because the project is ESM. Node's resolver does not guess extensions, and the specifier must
name the file that exists at runtime — which is the compiled .js. Leave it off and it
type-checks, builds, and dies at startup with ERR_MODULE_NOT_FOUND.
Why is my whitespace-only field passing validation?
Because @Length(1, 80) counts characters and the trim happens later, in the service.
Three spaces pass the validator and are stored as the empty string — the validator and the service
disagreed about what the value was.
The fix is a pipe that runs before validation, and the reason it is a good interview question is that it tests whether someone knows global pipes run in the order they are listed:
app.useGlobalPipes(new TrimPipe(), new ValidationPipe({ whitelist: true }))What does @Injectable() actually do?
Less than people assume. It marks the class so the metadata about its constructor parameters is
emitted — that is all. It does not register the class, which is what the module's
providers array is for.
The neat follow-up: a class with no dependencies works as a provider without
@Injectable(), because there is no metadata to emit. Add a constructor parameter and it
breaks. Which is why the decorator goes on every provider regardless.
Why does main.ts end with await bootstrap()?
Top-level await, which is legal in an ES module and not in CommonJS. It is a small
question that reveals whether someone has noticed the project is ESM — which is the same fact behind
the .js extensions, the imported-class entity list, and the migration scripts that
build first.
Two questions to ask them
Where do they draw the line between controller and service? The answer tells you whether their code is testable.
And what happens when a database constraint fires in production? Whether that is a 500 or a 409 says a lot about how much of the framework they have actually used.
That is the track. Start at the beginning if you skipped here, or go back to the request lifecycle, which is the lesson most of these answers come from.