cswork / libraryOpen practice workspace

Articles

NestJS Interview Questions: Dependency Injection, Guards, and Request Lifecycles

Prepare for NestJS interview questions with request traces, provider scope bugs, guard and pipe ordering, DTO validation, and focused testing examples.

NestJS interview questions often test whether you can follow a request through the application and explain which object owns each decision. Dependency injection supplies collaborators. Guards decide whether execution may proceed. Pipes validate or transform arguments. Interceptors wrap execution and can inspect or transform the result.

The difficult part is combining these features without placing request data in the wrong lifetime or expecting a later stage to fix an earlier decision. This guide uses a protected order endpoint to make those boundaries concrete, with debugging exercises you can explain without memorizing every decorator.

For each exercise below, write the expected execution order before changing the code. That small habit makes framework behavior much easier to reason about.

What is dependency injection in NestJS?

Dependency injection means a class declares the collaborators it needs and the application container supplies them. Instead of constructing a database client inside every service, a service receives an appropriate provider. This makes the dependency graph explicit and gives tests a controlled place to substitute behavior.

@Injectable()
export class OrdersService {
  constructor(private readonly repository: OrdersRepository) {}
}

The interview follow-up is usually about resolution: where is OrdersRepository registered, which module exports it, and which module imports that export? A TypeScript import merely makes a symbol available to the file. It does not make a Nest provider available in every module's dependency graph.

When Nest cannot resolve a dependency, identify the failing constructor parameter and injection token. Then inspect the module that creates that class. Guessing at extra imports until startup succeeds can hide duplicate registrations and accidentally create multiple instances of stateful providers.

How do default, request, and transient scopes differ?

A provider's scope controls its lifetime and sharing. The default scope generally gives one instance in the application's container context. Request scope creates instances associated with individual requests. Transient scope supplies a dedicated instance to each consuming injection context rather than one shared instance for everyone.

ScopeUseful exampleRisk to discuss
DefaultStateless pricing service or shared connection poolRequest-specific mutable fields can leak between requests
RequestA collaborator tied to the current request identityDependency chains may become request-scoped and cost more to construct
TransientA configurable helper owned by its consumerIt does not automatically mean a new object on every method call

Suppose a singleton OrdersService stores this.currentUserId when a request arrives. Request A sets the field, then awaits a database operation. Request B overwrites it before A continues. JavaScript's single-threaded execution does not prevent this interleaving across asynchronous work.

The simplest repair may be to pass the user or tenant identifier as a method argument. Request-scoped dependency injection can also be appropriate, but describe its cost and propagation. The goal is a correct ownership model, not using the most elaborate scope for every provider.

What order do guards, interceptors, pipes, and handlers run in?

For a typical HTTP request, Nest documents middleware before guards, followed by the incoming side of interceptors, pipes, and the route handler. Interceptors then process the outgoing result in reverse nesting order. Exceptions can divert this normal path toward applicable exception handling.

Remember the responsibility attached to each stage. Middleware handles early request processing. Guards make admission decisions using execution context. Pipes prepare route arguments. Interceptors wrap execution. The controller calls application logic and returns a result.

Consider an authentication guard and an integer parsing pipe on an order ID. The guard runs before the route argument pipe. It cannot assume that @Param('id', ParseIntPipe) has already produced a numeric argument. If a guard needs a route value for authorization, it must handle that boundary intentionally.

Global, controller, and route bindings also matter. Guards normally run from broader to narrower binding levels. Interceptors behave like nested wrappers, so their outbound order reverses. Within parameter-bound pipes there are additional ordering details; verify them when the exercise depends on parameter order rather than general stage order.

Trace a protected order request

Assume GET /orders/42 uses a global logging interceptor, an authentication guard, a route interceptor, and an integer parsing pipe. The handler fetches an order and returns a response DTO. A normal successful trace is:

middleware
authentication guard
global interceptor: before
route interceptor: before
parameter pipe
controller handler
route interceptor: after
global interceptor: after
response

Use the trace to answer a concrete question: why did the controller log never appear? If the authentication guard denied access, no handler execution is expected. If argument parsing failed, inspect the pipe error. If a route interceptor returned a cached result without calling the next handler, the missing log can be intentional.

A log line before next.handle() proves only that the interceptor entered. It does not prove the handler completed. Log successful completion, errors, and cancellation or finalization with the appropriate observable operators for the claim you want to make.

When should authorization live in a guard?

A guard is a natural place to decide whether a route may execute based on identity, roles, or policy metadata. The ExecutionContext lets it inspect the current handler and controller, which is useful for metadata-driven policies.

Role membership alone is often insufficient. A user with the customer role should not read another customer's order. The authorization check must include ownership or tenant membership. Avoid treating a client-supplied tenantId as trusted merely because a validation pipe accepted its string format.

For example, compare the authenticated principal's tenant with the order's tenant or constrain the database lookup by the authenticated tenant. If the guard and service both load the record, consider whether the repeated reads can disagree or waste work. A shared application policy or appropriately scoped loaded context can keep the contract explicit.

Also distinguish authentication failure from authorization failure in the response policy. Do not expose detailed resource existence to callers who should not know it. The exact status and message should follow the API contract, rather than whatever exception happens to be easiest to throw.

Why can a DTO look typed but still contain invalid input?

TypeScript's compile-time types do not validate arbitrary HTTP payloads at runtime. An incoming JSON value can contain the wrong type, missing fields, or extra properties regardless of the controller method's declared DTO type.

Nest's validation pipeline can apply runtime validation and configured transformation. Check the actual ValidationPipe options, DTO class metadata, and decorators. An interface disappears at runtime and cannot supply the same class metadata as a decorated DTO class.

Use small hostile examples in an interview: {"quantity":"two"}, {"quantity":-1}, or a body with an unexpected administrative property. Decide whether extra properties are ignored, removed, or rejected. Decide whether numeric strings should be converted or rejected. Those are API choices that deserve tests.

Do not confuse “well-formed payload” with “allowed operation.” A positive quantity may still exceed inventory. A valid email may still belong to another account. Pipes are helpful for argument shape; business invariants and authorization still require the corresponding application checks.

How do you test dependency injection and guards?

Start with a small testing module that declares the service and substitutes its external collaborator. Test the service contract with a fake repository that returns meaningful data and failures. This is different from testing that Nest bootstraps every production module correctly.

const moduleRef = await Test.createTestingModule({
  providers: [OrdersService, OrdersRepository],
})
  .overrideProvider(OrdersRepository)
  .useValue(fakeRepository)
  .compile();

Match the replacement token to the token actually injected. If production uses a string or symbol token, overriding an unrelated class token changes nothing. When testing scoped providers, use the supported resolution mechanism and request context instead of assuming a normal singleton lookup exercises request behavior.

A guard unit test can use a controlled execution context and principal. An HTTP integration test should verify registration and the full request outcome. Replacing a guard with an always-allow fake is useful for testing a controller in isolation, but that test provides no evidence that authorization works.

Debug an unexpected shared provider instance

Suppose two modules each declare AuditService in their own providers list. A developer expects a single central audit buffer, yet logs show separate counts. Inspect provider ownership before blaming asynchronous timing. Registering a provider in several module contexts can create separate instances.

Export the intended provider from its owning module and import that module where needed. Then verify the sharing contract with a simple instance identifier in a development test. Do not leave diagnostic identifiers or mutable global test state in production logic.

The reverse bug is just as instructive: one default-scoped object accidentally contains per-request data. Record two overlapping request IDs and observe whether one request sees another's value. A deterministic test can pause the first request at an await boundary, run the second request, then resume the first.

What makes a strong NestJS debugging answer?

Organize the explanation around evidence. First name the stage where behavior diverges. Next identify the provider token and lifetime involved. Then state the smallest test that separates the likely causes.

SymptomFirst checkUseful test
Cannot resolve dependencyToken, provider owner, module exports/importsCompile the smallest relevant testing module
Controller never runsGuard rejection, pipe failure, interceptor short circuitTrace stage entry and request outcome
Wrong user's dataShared mutable fields and trusted tenant sourceInterleave two authenticated requests
Validation silently missingRuntime DTO metadata and pipe registrationSend malformed JSON values through HTTP
Test replacement has no effectActual injection token and instance scopeAssert the fake receives the expected call

Avoid calling every component “middleware.” The names matter because their placement changes the evidence available and the work that has already happened. If the interviewer changes an HTTP controller to another transport, revisit assumptions about the request object and execution context.

A practical rehearsal you can finish in one sitting

Build a protected order lookup with one repository provider, one authentication guard, one parsing pipe, and two logging interceptors. Record the successful trace. Then introduce three controlled changes: deny the user, send a malformed order ID, and return a cached result from the inner interceptor.

For each change, predict which log lines disappear and why. Finally add two overlapping requests and check that request identity never lives in an unsafe shared field. This exercise connects dependency injection, guards, and request lifecycles in one small application instead of turning preparation into a decorator glossary.

When you answer aloud, begin with the requested behavior and the assumption that controls it. “The guard runs before route argument transformation, so I would not rely on that pipe to prepare the authorization input” is clearer than reciting the full lifecycle without connecting it to the bug.

What makes a useful request-lifecycle regression test?

Assert observable outcomes: an unauthorized request never reaches the service, invalid input produces the intended response, and an authorized valid request invokes the expected dependency. Avoid tests that merely reproduce the implementation's internal call sequence without checking its contract.

Sources and Further Reading

Research checked September 24, 2026. Examples assume Nest's HTTP application model; inspect transport and version-specific behavior when it affects the question.