Skip to content

Framework GuidesNestJS

NestJS

NestJS adapter: courierMiddleware serves the Courier proxy route, and requestContext carries the user's session in server code.

4 min readproxy mode

The @noverax/courier-nestjs adapter provides a middleware that integrates Courier into your NestJS application using the standard MiddlewareConsumer.

In one command

bash
npx @noverax/create-courier

In a NestJS app it adds a module with the proxy route and a page at /courier, imported into your root module, with a typed example API and its mock. See Create Courier.

Installation

bash
npm install @noverax/courier @noverax/courier-nestjs

Define your API sources

src/lib/sources.ts
import { courier, defineApi, defineGroup } from "@noverax/courier/define"

const { endpoint } = courier()

type User = { id: number; name: string; email: string }

const users = defineGroup({
  prefix: "users",
  endpoints: {
    getAll: endpoint.get("").response<User[]>(),
    getById: endpoint.get(":id").variables<{ id: number }>().response<User>(),
    create: endpoint.post("").variables<{ name: string; email: string }>().response<User>(),
    remove: endpoint.del(":id").variables<{ id: number }>().response<null>(),
  },
})

export const api = defineApi({
  baseURL: process.env.API_URL ?? "http://localhost:4000",
  groups: { users },
})

export const sources = { myApi: api }

Setup

Apply the courierMiddleware in your root AppModule using the MiddlewareConsumer:

src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common"
import { registerSources } from "@noverax/courier"
import { courierMiddleware } from "@noverax/courier-nestjs"
import { sources } from "./lib/sources"

registerSources(sources)

@Module({
  imports: [],
  controllers: [],
  providers: [],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(courierMiddleware())
      .forRoutes("api/request")
  }
}

The courierMiddleware function returns a NestJS-compatible middleware that reads sources from the core global registry populated by registerSources() at module load.

Request bodies

NestFactory.create parses JSON bodies for the whole application before any middleware runs, so a proxied JSON call reaches courierMiddleware already parsed. The middleware takes the body from Nest's parser, which stays on for your own controllers' @Body(). Create the app with rawBody: true and the proxy forwards the exact bytes the client sent; without it, the parsed JSON is serialized again, which gives the same value.

Nest's JSON parser accepts 100 KB by default — less than the proxy's own 1 MB maxBodyBytes — and answers a larger call with its own 413. Raise it to match:

src/main.ts
import { NestFactory } from "@nestjs/core"
import type { NestExpressApplication } from "@nestjs/platform-express"
import { AppModule } from "./app.module"

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, { rawBody: true })
  app.useBodyParser("json", { limit: "1mb" })
  await app.listen(3001)
}

bootstrap()

Uploads (multipart/form-data) are not parsed by Nest and reach the proxy as a stream, up to its maxUploadBytes — 10 MB unless you pass another to courierMiddleware.

With CORS

Enable CORS in your main.ts bootstrap:

src/main.ts
import { NestFactory } from "@nestjs/core"
import { AppModule } from "./app.module"

async function bootstrap() {
  const app = await NestFactory.create(AppModule)

  app.enableCors({
    origin: process.env.ALLOWED_ORIGINS?.split(",") ?? "http://localhost:5173",
    credentials: true,
  })

  await app.listen(3001)
}

bootstrap()

With existing modules

The Courier middleware sits alongside your existing NestJS controllers and modules:

src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common"
import { registerSources } from "@noverax/courier"
import { courierMiddleware } from "@noverax/courier-nestjs"
import { sources } from "./lib/sources"
import { HealthModule } from "./health/health.module"
import { StatsModule } from "./stats/stats.module"

registerSources(sources)

@Module({
  imports: [HealthModule, StatsModule],
  controllers: [],
  providers: [],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(courierMiddleware())
      .forRoutes("api/request")
  }
}

Middleware scope

The forRoutes("api/request") call limits the middleware to the proxy path only. It will not interfere with your other controllers or routes.

Using with a dedicated module

For larger applications, you can isolate the Courier setup into its own module:

src/courier/courier.module.ts
import { Module, NestModule, MiddlewareConsumer } from "@nestjs/common"
import { registerSources } from "@noverax/courier"
import { courierMiddleware } from "@noverax/courier-nestjs"
import { sources } from "../lib/sources"

registerSources(sources)

@Module({})
export class CourierModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(courierMiddleware())
      .forRoutes("api/request")
  }
}
src/app.module.ts
import { Module } from "@nestjs/common"
import { CourierModule } from "./courier/courier.module"
import { HealthModule } from "./health/health.module"

@Module({
  imports: [CourierModule, HealthModule],
})
export class AppModule {}

Guards and interceptors

NestJS guards and interceptors applied globally or to the api/request route will execute before the Courier middleware. This lets you add authentication or logging on top of the proxy route using standard NestJS patterns.

Studio integration & appOrigin

Wire your NestJS app into Studio with a single attachRuntime() call. In development it loads courier.config.ts from disk, registers its sources, and opens the connection to Studio; in production the app's own registerSources() call registers them. The appOrigin field for Playground proxy SSE/WS resolution lives in courier.config.ts.runtime.appOrigin (or set the COURIER_APP_URL env var to override per-environment).

The natural place to call attachRuntime is in main.ts, alongside bootstrap():

src/main.ts
import { NestFactory } from "@nestjs/core"
import { AppModule } from "./app.module"

async function bootstrap() {
  if (process.env.NODE_ENV !== "production") {
    const { attachRuntime } = await import("@noverax/courier-studio-core/runtime")
    await attachRuntime()
  }

  const app = await NestFactory.create(AppModule)
  await app.listen(3001)
}

bootstrap()

Alternatively, set COURIER_APP_URL instead of pinning appOrigin in the config:

bash
COURIER_APP_URL=http://localhost:3001 nest start

No magic default

There is no localhost:3001 auto-fallback. If appOrigin is missing, clicking "Subscribe" on an SSE/WS endpoint in Studio Playground proxy mode throws ConfigurationError("APP_ORIGIN_NOT_CONFIGURED") with a pointer back to this section. The correct value depends on your runtime — custom port, Docker host, HTTPS, reverse proxy — and a wrong guess would silently route requests to nowhere.

The user's session

An API that keeps a session gets the user's session with every call a guard, an interceptor, a pipe, a controller, a service or a step it awaits makes, once the request is served with it at hand. Mount the middleware as the application is created:

src/main.ts
import { NestFactory } from "@nestjs/core"
import { requestContext } from "@noverax/courier-nestjs"
import { AppModule } from "./app.module"

const app = await NestFactory.create(AppModule)
app.use(requestContext())
await app.listen(3000)

It runs before every module's own middleware, on the Express platform and on the Fastify platform alike. The session cookies the API sets reach the browser on the response, beside the ones the application sets — with res.cookie(), a @Header(), or a Fastify reply's headers. The user's address is taken from the socket, after the chain a proxy on a private network recorded. The request is kept in AsyncLocalStorage.

NestJS 10, 11 and 12 are checked on the Express platform, and 10 and 12 on the Fastify platform. See User Sessions.

Client-side setup

On the client, point your caller to this server's proxy URL. Choose the guide for your frontend:

  • React (Vite / CRA) — createProxyQueryCaller with React Query hooks
  • Vue — createProxyCaller with composables
  • Next.js — full-stack with apiRoute and createProxyQueryCaller
  • Nuxt — full-stack with courierHandler and createProxyCaller
lib/caller.ts
import { createProxyCaller } from "@noverax/courier"
import type { api } from "../shared/sources"

export const caller = createProxyCaller<typeof api>("api", {
  proxyPath: { http: "http://localhost:3001/api/request" },
})