NestJS – Interview Questions

September 5, 20268 min readUpdated 9/5/2026

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 -> filters

Middleware 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.