Writing1 min read

Architecture Notes

Next.js and NestJS in Production

When the request path carried work that belonged in the background.

The API timed out because the email job ran in the request

Users hit submit on onboarding. The Nest handler sent email, wrote audit rows, and called a webhook before responding.

Under load the p95 climbed. Next.js looked fine. The API was doing too much synchronously.


Web up front, API behind

We ran a full-stack product with Next.js for web and NestJS for APIs. SSR handled discovery and first paint. The API owned contracts and side effects.

The split was right. The boundaries inside the API were not.


Email ran inside the handler

Slow and retry-prone work lived in controllers. Users waited on jobs that could fail independently of their action.

One timeout produced duplicate retries upstream.


Fast answer, async work

Answer fast on the request path. Push notifications, imports, and webhook fan-out to a queue.

Make the handoff explicit in logs so ops can trace request ID to job ID.


Controller to queue handoff

Request path
Thin controller
@Post('signup')
async signup(@Body() dto: SignupDto) {
  const user = await this.users.create(dto);
  await this.queue.enqueue('send-welcome', { userId: user.id });
  return { id: user.id, status: 'pending_email' };
}

Split the request path

Visible boundaries matter more than labels. Answer fast for users. Be honest about async work.


References