18 questions

NestJS interview questions and answers

Middle questions

  1. 1. After an incident you find that a NestJS API returned a database driver error, with the password from the connection string, to the client in a 500 response. How could that happen if Nest's default filter hides details, and how do you make it impossible?

    Answer

    The built-in exception filter really does return only 500 and Internal server error for a plain Error and writes the text to the log. The leak happens when someone wraps the error by hand: throw new InternalServerErrorException(err.message) or new HttpException(err, 500) turn the message or the passed object into the response body, or a custom filter returns exception.message or stack. Protection: one global filter registered with APP_FILTER that returns a generic body with a requestId for anything except known HttpException and domain errors, and writes the full error to the log with secrets redacted. Plus a review or lint rule against passing raw errors into HttpException, and a test that a 500 body has no details. And the connection string itself must not end up in error texts and logs.

    Follow-up question: How do you return validation errors with field details without exposing details of other errors?

  2. 2. In a NestJS project the JWT and role check was done in Express middleware, and a separate map of paths to roles had to be maintained. Why does Nest have guards for this, and what does a guard see that middleware does not?

    Answer

    Middleware runs before Nest routing and only knows req and res, so it does not know which controller and handler will process the request, and roles have to be duplicated in a separate path map. A guard gets the ExecutionContext: through context.getHandler() and getClass() it reads route metadata, and Reflector pulls the roles from a @Roles('admin') decorator right above the method. The rule lives next to the code and does not drift from the routes after a rename. A guard also works the same way for HTTP, WebSockets and microservices through switchToHttp() or switchToWs(). Middleware stays for things that do not depend on the route: logging, helmet, a cookie parser.

    Follow-up question: A global guard is registered with app.useGlobalGuards(new AuthGuard()), and its Reflector is undefined. Why?

  3. 3. In NestJS, UsersService depends on MailService, which is declared in providers of MailModule, and UsersModule has imports: [MailModule]. The app does not start with Nest can't resolve dependencies. Why, and how do you fix it?

    Answer

    A module in Nest is a visibility boundary: a module's providers are private until they are added to exports. The entry imports: [MailModule] gives access only to what that module exports, and MailModule exports nothing. That is why UnknownDependenciesException appears. The one-line fix is to add exports: [MailService] to MailModule. Duplicating MailService in the providers of UsersModule is not advisable, because that creates a second instance.

    Follow-up question: When is it right to make a module @Global(), and when does it hide dependencies?

  4. 4. In a NestJS integration test you need to replace PaymentsClient, which calls the real payment API, with a fake while keeping the rest of the module real. How do you do it, and when would you use useValue versus useClass or useFactory?

    Answer

    Build the module with Test.createTestingModule({ imports: [PaymentsModule] }) and before compile() call .overrideProvider(PaymentsClient).useValue(fake). The override works by token, so if the service injects the client with @Inject(PAYMENTS_CLIENT), that token is the one to override. useValue suits a ready object with jest.fn() or mock.fn(): Nest injects nothing into it. useClass gives a fake class that Nest passes dependencies into, and useFactory with inject is for a fake built from other providers, for example from config. providers: [PaymentsClient] in a module is only shorthand for { provide: PaymentsClient, useClass: PaymentsClient }, so the substitution mechanism is the same as in production.

    Follow-up question: How do you make sure the real PaymentsClient cannot be called by accident in any test?

  5. 5. In NestJS, CheckoutService depends on the interface PaymentGateway, and providers contains the class LiqPayGateway that implements it. Startup fails with Nest can't resolve dependencies. Why, and how do you fix it while keeping the dependency on an abstraction?

    Answer

    TypeScript interfaces do not exist at runtime, so Nest has no token: in the constructor metadata an interface is recorded simply as Object. You need an explicit token: a Symbol, a string or an abstract class. You register { provide: PAYMENT_GATEWAY, useClass: LiqPayGateway } and write @Inject(PAYMENT_GATEWAY) in the constructor. That way the service depends on an abstraction, and the implementation is easy to swap in tests or by config.

    Follow-up question: What breaks if you import the service with import type?

  6. 6. In what order are lifecycle hooks called in NestJS at startup and at shutdown? Where is it better to open a database connection: in the constructor or in onModuleInit?

    Answer

    The constructor runs when the app is created, while onModuleInit and onApplicationBootstrap are called at listen or init. Startup order: constructor, onModuleInit, onApplicationBootstrap. Shutdown goes: onModuleDestroy, beforeApplicationShutdown, onApplicationShutdown. An async onModuleInit delays startup, because Nest waits for its Promise. That is why connections are opened in onModuleInit and not in the constructor, which cannot be async. System signals work only after enableShutdownHooks().

    Follow-up question: Are these hooks called for a request-scoped class?

  7. 7. Name the full request processing chain in NestJS in the correct order. Where do exception filters work?

    Answer

    The order is: middleware, guards, interceptors (the part before the handler), pipes, the handler itself, then interceptors on the way back. A pipe runs after an interceptor, not before it. An interceptor wraps the handler call: code before next.handle() runs on the way in, code in tap or map on the way out. Exception filters fire only when an exception was thrown and not caught somewhere in this chain. Within each type the order is: global, then controller, then route.

    Follow-up question: Why are exception filters looked up in reverse, from route to global?

  8. 8. What do the whitelist, forbidNonWhitelisted and transform options do in the NestJS ValidationPipe? What happens to an extra role field in the body and to page=2 in the query?

    Answer

    With whitelist: true the pipe silently strips fields that have no validation decorators, so the extra role in the body disappears. forbidNonWhitelisted: true returns 400 saying the property should not exist instead of stripping it. With transform: true the pipe turns a plain object into an instance of the DTO class, and with @Type(() => Number) the string page=2 from the query becomes a number. Without whitelist extra fields pass through and create a mass assignment vulnerability.

    Follow-up question: Why is the value in the DTO not a class instance without transform?

  9. 9. ValidationPipe is enabled, but POST /orders with items: [{ sku: 5, qty: -1 }] returns 201. CreateOrderDto has only @IsArray(). Why did the ItemDto rules not fire, and what should you add?

    Answer

    @IsArray() only checks that the value is an array and does not look inside the elements. The ItemDto[] type in TypeScript disappears at runtime, so the validator does not know what these elements are. You need @ValidateNested({ each: true }) to validate every element, and @Type(() => ItemDto) from class-transformer so the elements become class instances with metadata. Without @Type the elements stay plain objects and the decorators on them have no effect.

    Follow-up question: How do you limit the array size and validate a union in a nested field?

  10. 10. How do you organize configuration in NestJS so the app does not start without DATABASE_URL instead of failing on the first request?

    Answer

    You add ConfigModule.forRoot({ isGlobal: true }) and pass a validationSchema (for example Joi) or a validate function: environment variables are checked at startup and the app fails right away with a clear error. In code you use ConfigService.getOrThrow, which throws if the value is missing. With registerAs the config is grouped into a namespace. Note that registerAs validates nothing by itself, the check is still done by the schema or validate.

    Follow-up question: How do you pass the config to TypeOrmModule.forRootAsync and change a value in a test?

  11. 11. Walk a request through authentication with Passport and JWT in NestJS, from login to req.user in the controller. What must never go into the token?

    Answer

    At login the server compares the password with the hash and signs a JWT via JwtService. On a protected route AuthGuard('jwt') runs JwtStrategy: it takes the Bearer token from the header and verifies the signature and exp. Then validate(payload) is called and its result becomes req.user. A JWT is signed but not encrypted, so its contents can be read without a key, and passwords and sensitive data are never put into it. Authentication answers "who are you", authorization answers "what are you allowed to do".

    Follow-up question: How do you revoke an already issued access token?

  12. 12. All endpoints of a NestJS API must require a JWT by default, except /health and /auth/login. How do you do this so a new controller cannot be accidentally left open?

    Answer

    You register a global guard through the APP_GUARD provider instead of @UseGuards on every controller, so protection applies by default. Exceptions are marked with a custom @Public() decorator that sets a flag via SetMetadata. The guard reads it with Reflector.getAllAndOverride on the handler and the class. Payment provider webhooks are also public as far as JWT is concerned, but they must verify the request signature. On top of that you write a test that walks all routes and expects 401 without a token, except for an allowlist.

    Follow-up question: How do you support both JWT for users and API keys for partners in the same guard?

  13. 13. You are writing an internal NestJS module for a payment provider client. Three services use it with different keys, and the key must come from ConfigService. How do you make the module configurable?

    Answer

    Make it a dynamic module: a static method returns a DynamicModule with module, an options provider under its own token ({ provide: PAYMENTS_OPTIONS, useValue: options }), a client that injects those options, and exports. Since the key depends on another provider, you need the async variant: forRootAsync({ imports: [ConfigModule], useFactory: (config) => ({ apiKey: config.getOrThrow('PAY_KEY') }), inject: [ConfigService] }). The names carry meaning: forRoot is configured once at the root, register gives a separate instance per import. To avoid writing this by hand there is ConfigurableModuleBuilder, which generates typed register and registerAsync. @Global() is not needed here: an explicit import shows who depends on payments.

    Follow-up question: How do you test a service that uses this module with fake options without bringing up ConfigModule?

  14. 14. OrdersModule imports UsersModule, and UsersModule already imported OrdersModule, and NestJS crashes at startup with an error about undefined in imports. How do you fix it quickly, and how properly?

    Answer

    A cycle between modules or between files (often through a barrel index.ts) makes the class undefined at the moment the decorator is applied. The quick fix is forwardRef(() => Module) in both modules and @Inject(forwardRef(...)) for providers. But the creation order in a cycle is undefined, so you cannot rely on a dependency in the constructor. It is better to break the cycle: move shared logic into a third module or reverse the dependency via events. Cycles between files are found with a tool like madge --circular.

    Follow-up question: Does the caller of emit learn about an error in an async EventEmitter2 listener?

  15. 15. A money transfer in NestJS "half succeeds": the debit happened, the credit failed, but there is no rollback. Inside dataSource.transaction(...) some queries go through an injected repository. What is the cause?

    Answer

    The injected repository works through a different connection from the pool, so its queries run outside the transaction and rollback does not touch them. All operations inside the transaction must go through the manager given by the callback: manager.decrement(...), manager.save(...). Rollback undoes only what went through that manager. In Prisma the rule is the same: queries go through tx in $transaction(async (tx) => ...), not through the global client.

    Follow-up question: How do you pass the transaction through several services without dragging manager into every method?

  16. 16. Services throw domain errors OrderNotFound and InsufficientFunds, and Prisma throws P2002 on a duplicate. How do you design error handling in NestJS so the client gets correct statuses in a uniform format?

    Answer

    Domain errors do not extend HttpException, so the domain layer does not depend on HTTP. One global exception filter maps error types to statuses: OrderNotFound to 404, InsufficientFunds to 422 or 409, Prisma P2002 to 409, P2025 to 404. The response body has a uniform format, for example Problem Details with a requestId. Unknown errors are returned as 500 with a generic text, and the full stack trace goes only to the log. The filter is registered via APP_FILTER so DI works.

    Follow-up question: How do you handle the same set of domain errors in a queue consumer, where there are no HTTP statuses?

  17. 17. What scope do NestJS providers have by default, and what does that mean for state in class fields? When is REQUEST scope needed, what does it cost, and what can you use instead?

    Answer

    By default providers are singletons: one instance per application. So request state in the fields of such a service gets mixed between concurrent requests. Scope.REQUEST creates an instance per request but "bubbles up" the dependency chain: everything that depends on it becomes request-scoped too. The cost is extra allocations and slower requests. For request context (id, tenant, user) it is better to use AsyncLocalStorage. A cron job or queue consumer that depends on a request-scoped provider has no HTTP request, and that causes an error.

    Follow-up question: What happens to a cron job if it depends on a request-scoped provider?

  18. 18. A NestJS latency metrics interceptor always logs took 0ms: it calls next.handle() and logs the difference between Date.now() and start right away, before the result is returned. What is wrong, how do you rewrite it, and which requests will it not see?

    Answer

    next.handle() returns a lazy Observable: the handler runs only on subscription, that is after intercept has already returned. So the time difference is almost zero. You must measure inside the stream: next.handle().pipe(tap(...)), and for errors add error handling or finalize. The error status is set later by the exception filter, so you take it from the error itself. Guards run before interceptors, so 401 and 403 responses will not reach this metric. For the full picture you add middleware with res.on('finish').

    Follow-up question: How do you add a 5 second timeout to the handler in an interceptor, and what happens to the work the handler already started?

Train these questions

Study the cards, answer by text or voice and get an AI grade. Needs a signed in account.

Answer out loud and get graded

Here you get the questions with model answers. In the ShortLesson app you answer yourself, by text or voice, and AI grades the answer against the rubric and asks a follow-up, like a real interviewer.

The app is coming to the stores soon.

Other topics