> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useshipd.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Node server example

> A complete Hono server on Node 22 that evaluates shipd flags per request.

## Overview

A complete Node 22 API built with [Hono](https://hono.dev). It creates one shipd client per process, waits for `ready()` before it listens, evaluates `new-checkout` and `search-variant` per request, and closes the client on shutdown.

## What's in it

* `shipd.config.ts` defines two flags. `shipd push` syncs them and writes `src/generated/flags.ts`.
* `src/lib/shipd.ts` creates a singleton client with a server SDK key (`shipd_sdk_server_…`).
* A middleware builds the evaluation context from the `x-user-id` header or the `userId` query parameter.
* `SIGINT` / `SIGTERM` stop the HTTP server, then call `shipd.close()`.

```text theme={null}
node-example/
├── .env.example
├── package.json
├── shipd.config.ts
├── tsconfig.json
└── src/
    ├── generated/
    │   └── flags.ts
    ├── lib/
    │   └── shipd.ts
    └── server.ts
```

## Files

```json package.json theme={null}
{
  "name": "node-example",
  "private": true,
  "type": "module",
  "engines": {
    "node": ">=22"
  },
  "scripts": {
    "dev": "tsx watch --env-file=.env src/server.ts",
    "start": "tsx --env-file=.env src/server.ts",
    "check-types": "tsc --noEmit",
    "flags:diff": "shipd diff",
    "flags:push": "shipd push"
  },
  "dependencies": {
    "@hono/node-server": "^1.19.0",
    "hono": "^4.9.0",
    "shipdit": "^0.4.0"
  },
  "devDependencies": {
    "@shipdit/cli": "^0.3.2",
    "@types/node": "^22.18.0",
    "tsx": "^4.20.0",
    "typescript": "^5.9.0"
  }
}
```

```json tsconfig.json theme={null}
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["shipd.config.ts", "src"]
}
```

```ts shipd.config.ts theme={null}
import { defineFlags } from "shipdit/config";

export default defineFlags({
  "new-checkout": {
    type: "boolean",
    default: false,
    description: "Redesigned checkout flow",
    tags: ["checkout"],
  },
  "search-variant": {
    type: "variant",
    variants: ["control", "semantic", "hybrid"] as const,
    default: "control",
    description: "Search ranking experiment",
    tags: ["search", "experiment"],
  },
});
```

```bash .env.example theme={null}
# Server SDK key from the dashboard: project → SDK keys → Create SDK key (Key type: server)
SHIPD_SDK_KEY=shipd_sdk_server_...
PORT=3000
```

`shipd push` and `shipd types gen` write this file. Commit it and do not edit it by hand.

```ts src/generated/flags.ts theme={null}
/* eslint-disable */
/* This file is generated by shipd types gen. Do not edit. */

export interface FlagDefinitions {
  "new-checkout": {
    type: "boolean";
    default: false;
  };

  "search-variant": {
    type: "variant";
    variants: "control" | "semantic" | "hybrid";
    default: "control";
  };
}

export type FlagKey = keyof FlagDefinitions;

export type BooleanFlagKey = {
  [K in FlagKey]: FlagDefinitions[K]["type"] extends "boolean" ? K : never;
}[FlagKey];

export type VariantFlagKey = {
  [K in FlagKey]: FlagDefinitions[K]["type"] extends "variant" ? K : never;
}[FlagKey];

export type NumberFlagKey = {
  [K in FlagKey]: FlagDefinitions[K]["type"] extends "number" ? K : never;
}[FlagKey];

export type JsonFlagKey = {
  [K in FlagKey]: FlagDefinitions[K]["type"] extends "json" ? K : never;
}[FlagKey];
```

```ts src/lib/shipd.ts theme={null}
import { createClient } from "shipdit";

import type { FlagDefinitions } from "../generated/flags";

const sdkKey = process.env.SHIPD_SDK_KEY;
if (!sdkKey) {
  throw new Error("SHIPD_SDK_KEY is not set");
}

// One client per process. It downloads the snapshot, keeps it in memory,
// and evaluates every flag check locally.
export const shipd = createClient<FlagDefinitions>({
  sdkKey,
  // Node 22 has a global WebSocket, so realtime invalidation works without extra packages.
  stream: true,
  defaults: {
    "new-checkout": false,
    "search-variant": "control",
  },
  onError: (error) => {
    console.error("[shipd]", error);
  },
});
```

```ts src/server.ts theme={null}
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import type { EvaluationContext } from "shipdit";

import { shipd } from "./lib/shipd";

type Env = { Variables: { flagContext: EvaluationContext } };

const app = new Hono<Env>();

// Build the evaluation context once per request.
app.use("*", async (c, next) => {
  const userId = c.req.header("x-user-id") ?? c.req.query("userId");
  const plan = c.req.header("x-plan");
  c.set("flagContext", {
    userId,
    attributes: plan ? { plan } : {},
  });
  await next();
});

app.get("/checkout", (c) => {
  const newCheckout = shipd.isEnabled("new-checkout", c.get("flagContext"));
  return c.json({ checkout: newCheckout ? "new" : "legacy" });
});

app.get("/search", (c) => {
  const q = c.req.query("q") ?? "";
  // "control" | "semantic" | "hybrid"
  const engine = shipd.getVariant("search-variant", c.get("flagContext"));
  return c.json({ q, engine, results: [] });
});

app.get("/healthz", (c) => c.json({ ok: true, snapshotVersion: shipd.getVersion() }));

// ready() resolves after the first snapshot or after readyTimeoutMs (5s).
// It never rejects for network errors; until a snapshot arrives you get `defaults`.
await shipd.ready();

const port = Number(process.env.PORT ?? 3000);
const server = serve({ fetch: app.fetch, port }, (info) => {
  console.log(`Listening on http://localhost:${info.port}`);
});

function shutdown(signal: NodeJS.Signals) {
  console.log(`${signal} received, shutting down`);
  server.close(() => {
    // Stops polling and the stream, and flushes queued evaluation events.
    shipd.close();
  });
}

process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
```

## Run it

```bash theme={null}
pnpm install
cp .env.example .env        # paste your server SDK key
pnpm exec shipd login       # approve org → project → environment in the browser
pnpm exec shipd push        # creates the flags and rewrites src/generated/flags.ts
pnpm dev
```

```bash theme={null}
curl -H "x-user-id: usr_8f2k19" http://localhost:3000/checkout
# {"checkout":"legacy"}

curl "http://localhost:3000/search?q=boots&userId=usr_8f2k19"
# {"q":"boots","engine":"control","results":[]}
```

Pushed flags serve their `default`. To see `{"checkout":"new"}`, open `new-checkout` in the dashboard, add a rule or change **Fallthrough** to `true`, and click **Save environment**. The server picks up the new snapshot without a restart.

## Adapt it

* Add attributes to the evaluation context from your auth layer (`plan`, `country`, `organizationId`). The server SDK does not infer location.
* Percentage rollouts need a `userId`. Requests without one fall through.
* On Node 20, pass `WebSocket` from the `ws` package or omit `stream`. Polling still runs every 30 seconds.
* Omit `stream` to poll only. Set `refreshIntervalMs: 0` for one fetch per process, for example in short-lived jobs.

## Next

* [Node SDK guide](/sdks/node)
* [SDK reference](/reference/sdk)
* [SDK keys](/concepts/keys)
* [Generated types](/config-as-code/types)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.