Search docs

Jump between documentation pages.

Getting started

Build a tiny DaloyJS server, hit it with the typed client, and inspect the OpenAPI spec — in five minutes.

1. Scaffold

bash
mkdir hello-daloy && cd hello-daloy
pnpm init
pnpm add @daloyjs/core zod
pnpm add -D typescript tsx @types/node
json
// package.json — add these
{
  "type": "module",
  "scripts": {
    "dev": "node --import tsx/esm --watch src/index.ts",
    "start": "node --import tsx/esm src/index.ts"
  }
}

We use the explicit tsx/esm loader subpath because the project is "type": "module". The bare --import tsx form also works on recent Node versions, but tsx/esm is the canonical entrypoint for ESM projects and avoids loader-resolution surprises in stricter setups.

We use src/index.ts and --watch here so the layout matches what create-daloy emits — copy/paste between this guide and a scaffolded project without renaming files.

2. Write your first route

ts
// src/index.ts
import { z } from "zod";
import { App, requestId, secureHeaders } from "@daloyjs/core";
import { serve } from "@daloyjs/core/node";

const app = new App({
  bodyLimitBytes: 64 * 1024,
  requestTimeoutMs: 5_000,
});

app.use(requestId());
app.use(secureHeaders());

app.route({
  method: "GET",
  path: "/greet/:name",
  operationId: "greet",
  tags: ["Demo"],
  request: { params: z.object({ name: z.string().min(1) }) },
  responses: {
    200: { description: "Greeting", body: z.object({ msg: z.string() }) },
  },
  handler: async ({ params }) => ({
    status: 200,
    body: { msg: `Hello, ${params.name}!` },
  }),
});

const { port } = serve(app, { port: 3000 });
console.log(`listening on http://localhost:${port}`);

Prefer the colorized startup panel you get from create-daloy templates? Swap the plain console.log for printStartupBanner() from @daloyjs/core/banner — it renders a TTY-aware, ASCII-fallback boxed banner with your app name, URL, and any extra links (Swagger UI, health check, etc.):

ts
import { printStartupBanner } from "@daloyjs/core/banner";

const { port } = serve(app, { port: 3000 });
printStartupBanner({
  name: "MyAPI",
  version: "1.0.0",
  url: `http://localhost:${port}`,
  runtime: "Node.js",
  links: [
    { label: "Swagger UI", url: `http://localhost:${port}/docs` },
    { label: "OpenAPI JSON", url: `http://localhost:${port}/openapi.json` },
    { label: "Health", url: `http://localhost:${port}/healthz` },
  ],
});
bash
pnpm dev
# in another shell
curl http://localhost:3000/greet/world
# → {"msg":"Hello, world!"}

Don't want to spin up a real server? Every App exposes app.request(input, init?), an in-process test client that takes a URL or Request and returns a Response— no network stack, no port, no second terminal. It's the same entrypoint the typed client and testing guide use:

ts
const res = await app.request("/greet/world");
console.log(res.status, await res.json());
// → 200 { msg: "Hello, world!" }

3. Add OpenAPI & docs UI

ts
import { generateOpenAPI } from "@daloyjs/core/openapi";
    import { swaggerUiHtml, htmlResponse } from "@daloyjs/core/docs";

app.route({
  method: "GET",
  path: "/openapi.json",
  operationId: "openapi",
  responses: { 200: { description: "OpenAPI doc" } },
  handler: async () => ({
    status: 200,
    body: generateOpenAPI(app, { info: { title: "Hello", version: "1.0.0" } }),
  }),
});

app.route({
  method: "GET",
  path: "/docs",
  operationId: "docs",
  responses: { 200: { description: "API reference" } },
  handler: async () => {
    const html = swaggerUiHtml({ specUrl: "/openapi.json", title: "Hello API" });
    const res = htmlResponse(html);
    return { status: 200, body: await res.text(), headers: Object.fromEntries(res.headers) };
  },
});

Open http://localhost:3000/docs for interactive Swagger UI.

Both swaggerUiHtml() and scalarHtml() load their default assets from the jsDelivr CDN, so a strict Content-Security-Policy must allow those assets or the docs UI can render blank. htmlResponse() adds a compatible CSP automatically; if you build your own response, import docsContentSecurityPolicy from @daloyjs/core/docs and pass the result as the response header:

ts
import { docsContentSecurityPolicy } from "@daloyjs/core/docs";

headers: { "content-security-policy": docsContentSecurityPolicy() }

4. Use the typed in-process client

ts
import { createClient } from "@daloyjs/core/client";

const client = createClient(app, { baseUrl: "http://localhost:3000" });
const r = await client.greet({ params: { name: "DaloyJS" } });
//    ^? { status: 200; body: { msg: string } }
console.log(r.status, r.body);

5. Generate a Hey API SDK

For consumers outside the monorepo, generate a fully typed fetch SDK:

bash
pnpm add -D @hey-api/openapi-ts
ts
// openapi-ts.config.ts
import { defineConfig } from "@hey-api/openapi-ts";
export default defineConfig({
  input: "./generated/openapi.json",
  output: { path: "./generated/client", format: "prettier" },
  plugins: ["@hey-api/client-fetch", "@hey-api/typescript", "@hey-api/sdk"],
});
bash
pnpm exec openapi-ts

Next steps