Skip to content

Framework GuidesFastify

Fastify

Fastify server adapter using courierPlugin for hosting the Courier proxy route.

3 min readproxy mode

The @noverax/courier-fastify adapter provides a Fastify plugin that registers the Courier proxy route.

In one command

bash
npx @noverax/create-courier

In a Fastify server it adds a plugin with the proxy route and a page at /courier — loaded by itself in an app fastify-cli made — with a typed example API and its mock. See Create Courier.

Installation

bash
npm install @noverax/courier @noverax/courier-fastify fastify @fastify/cors

Define your API sources

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

Create a Fastify server with the Courier plugin:

server.ts
import Fastify from "fastify"
import cors from "@fastify/cors"
import { registerSources } from "@noverax/courier"
import { courierPlugin } from "@noverax/courier-fastify"
import { sources } from "./lib/sources"

registerSources(sources)

const app = Fastify({ logger: true })

async function start() {
  await app.register(cors, {
    origin: "http://localhost:5173",
    credentials: true,
  })

  // Register the Courier proxy plugin
  await app.register(courierPlugin, {
    prefix: "/api/request",
  })

  await app.listen({ port: 3001 })
}

start()

The courierPlugin registers a route handler at the specified prefix that reads sources from the core global registry populated by registerSources() at module load.

With existing Fastify app

Add the Courier plugin alongside your existing routes and plugins:

server.ts
import Fastify from "fastify"
import cors from "@fastify/cors"
import { registerSources } from "@noverax/courier"
import { courierPlugin } from "@noverax/courier-fastify"
import { sources } from "./lib/sources"

registerSources(sources)

const app = Fastify({ logger: true })

async function start() {
  await app.register(cors, {
    origin: "http://localhost:5173",
    credentials: true,
  })

  // Your existing routes
  app.get("/health", async () => {
    return { status: "ok" }
  })

  app.get("/api/stats", async () => {
    return { uptime: process.uptime() }
  })

  // Register Courier proxy plugin
  await app.register(courierPlugin, {
    prefix: "/api/request",
  })

  await app.listen({ port: 3001 })
}

start()

Plugin encapsulation

The courierPlugin follows Fastify's plugin system. It encapsulates its routes under the given prefix without interfering with your other routes or plugins.

CORS configuration

For production, restrict the allowed origins:

server.ts
import Fastify from "fastify"
import cors from "@fastify/cors"
import { registerSources } from "@noverax/courier"
import { courierPlugin } from "@noverax/courier-fastify"
import { sources } from "./lib/sources"

registerSources(sources)

const app = Fastify({ logger: true })

async function start() {
  await app.register(cors, {
    origin: process.env.ALLOWED_ORIGINS?.split(",") ?? "http://localhost:5173",
    credentials: true,
  })

  await app.register(courierPlugin, {
    prefix: "/api/request",
  })

  await app.listen({
    port: Number(process.env.PORT) || 3001,
    host: "0.0.0.0",
  })
}

start()

Fastify performance

Fastify's schema-based validation and serialization make it one of the fastest Node.js frameworks. The Courier plugin benefits from Fastify's built-in JSON parsing, so no additional body parser setup is needed.

The user's session

An API that keeps a session gets the user's session with every call a route, a service or a step it awaits makes, once the request is served with it at hand:

server.ts
import { requestContext } from "@noverax/courier-fastify"

app.addHook("onRequest", requestContext())

Add the hook on the root instance, where it runs for every route whichever plugin registers it. The session cookies the API sets reach the browser on the reply, beside any your route sets, and the user's address follows the rule the proxy route follows. Fastify 4 and 5 are checked. See User Sessions.

Studio integration & appOrigin

Wire your Fastify server 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).

server.ts
import Fastify from "fastify"
import { courierPlugin } from "@noverax/courier-fastify"

const app = Fastify()

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

  await app.register(courierPlugin, { prefix: "/api/request" })

  await app.listen({ port: 3001 })
}

start()

Alternatively, set COURIER_APP_URL instead of passing appOrigin explicitly:

bash
COURIER_APP_URL=http://localhost:3001 node server.js

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.

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" },
})