Getting startedScaffolderDX

Scaffolding a Production-Ready DaloyJS App in 60 Seconds with create-daloy

A tour of pnpm create daloy@latest — the interactive template + package-manager pickers, --minimal, --with-ci, the five runtime templates (Node, Bun, Deno, Workers, Vercel Edge), the AGENTS.md + .agents/skills/daloyjs-best-practices/SKILL.md drop-in for coding agents, and the printStartupBanner() polish that ships with every scaffold.

Devlin DuldulaoFullstack cloud engineer11 min read

Hi, Devlin. Ten years of fullstack, currently in Norway. I have spent a frankly embarrassing amount of my career on the first sixty seconds of a project — the part where you go from I have an idea to I have a running server with tests, types, OpenAPI, and a CI pipeline. When that first sixty seconds is awkward, the project never happens. When it feels good, you keep going.

create-daloy is the scaffolder we shipped to make those sixty seconds feel good for DaloyJS. This post is the grand tour: every template, every flag, the AGENTS.md drop-in for coding agents, and the cosmetic-but-important printStartupBanner()that tells you the dev server is alive. Then, at the end, I'll hand you off to the contract-first post for the next sixty seconds — wiring up the typed client.

The 60-second path

~/code · zsh
bash
# The 60-second path.
pnpm create daloy@latest my-api
cd my-api
pnpm dev
# → server up at http://localhost:3000
# → /docs serving Swagger UI
# → /openapi.json serving the contract
# → secure headers, CSRF, session, rate-limit middleware preloaded
default template · default package manager · sensible defaults

That's the entire story for most people. One command. You get an HTTP server, an OpenAPI document, Swagger UI, hardened security middleware preloaded (CSRF, sessions, secure headers, rate limit), a sensible folder structure, and a CI pipeline that's pinned and sandboxed. The scaffolder is opinionated for you so you can start making the opinionated decisions about your own app.

The interactive flow, when you don't pass arguments

If you skip the project name, create-daloywalks you through a tiny terminal wizard — template picker, package manager picker, git/CI yes-or-no. Arrow keys, enter, done. It's the part I personally use most because I always forget which template ID maps to Workers vs Vercel.

~/code · zsh · interactive
bash
$ pnpm create daloy@latest

  create-daloy scaffold a DaloyJS project

  ?  Project name my-api
  ?  Template
 node-basic         Traditional REST API with secure defaults and Hey API codegen
       vercel-edge        Catch-all Vercel Edge route with Node.js migration notes
       cloudflare-worker  Worker entrypoint with wrangler dev/deploy scripts
       bun-basic          Bun-native server with `bun --hot`, `bun test`, and Hey API codegen
       deno-basic         Deno-native server with `deno task dev`, `deno test`, and `npm:` imports
  ?  Package manager
 pnpm     Recommended default with the hardened pnpm workspace settings
       npm      Use the stock npm CLI with rewritten scripts and docs
       yarn     Yarn workflow with rewritten scripts and lockfile-friendly installs
       bun      Bun package manager for fast installs; runtime templates stay Bun-native
  ?  Initialize a git repository?               (Y/n)  Y
  ?  Add hardened GitHub Actions and security?  (Y/n)  Y
  ?  Install dependencies now?                  (Y/n)  N   # pnpm respects minimumReleaseAge

  Wrote 38 files
  Initialized git repo
  Wrote .github/ workflows, CODEOWNERS, SECURITY.md

  Next steps:
    cd my-api
    pnpm install                # honors the 24h release-age cooldown
    pnpm dev                    # starts the dev server on :3000
    open http://localhost:3000/docs
arrow keys to pick · ✓ to confirm · ↩ for default

One detail worth pointing out: when the package manager is pnpm, --install defaults to N. That's on purpose. The scaffolded project ships a .npmrc with minimum-release-age=1440 and a pnpm-workspace.yaml with blockExoticSubdeps: true and strictDepBuilds: true. The first install needs to honor those, not race past them. So we let you cd in, look at the files, and run pnpm install deliberately. Five seconds slower; way fewer surprises.

The full flag surface

pnpm create daloy@latest --help
bash
# Every flag, when you want them.

pnpm create daloy@latest [project-name] [options]

  --template <name>          node-basic | vercel-edge | cloudflare-worker | bun-basic | deno-basic
                             (default: node-basic)
  --package-manager <pm>     pnpm | npm | yarn | bun       (default: pnpm)
  --list-templates           Print available templates and exit.
  --install / --no-install   Install deps after scaffolding.
                             (default: Y, except pnpm N to respect
                             minimumReleaseAge + onlyBuiltDependencies)
  --git / --no-git           Initialize a git repository.
  --minimal                  Strip the bookstore + OpenAPI docs demo routes.
  --with-ci / --no-ci        Add hardened GitHub Actions + governance files.
                             (default: Y)
  --code-owner <owner>       CODEOWNERS owner for --with-ci, e.g. @acme/security.
  --force                    Overwrite an existing non-empty directory.
  --yes, -y                  Accept all defaults; never prompt.
  --help, -h                 Print this help.
--help · all flags · sensible defaults already chosen

--minimal vs the bookstore demo

By default the scaffold drops in a small bookstore API — books, authors, reviews — that exercises every contract-first pattern we want you to copy: nested resources, JSON Schema validation, pagination, typed errors, integration tests. Read it twice, then either delete the routes by hand or pass --minimal and start from a single /healthz route.

FILES.md
bash
# What you get out of the box

  WITHOUT --minimal (the default)        |   WITH --minimal
  -------------------------------------- + --------------------------------------
  src/app.ts                              |   src/app.ts
    + buildApp()                          |     + buildApp()
    + secureHeaders, csrf, session,       |     + secureHeaders, csrf, session,
      rateLimit, cors, requestId,         |       rateLimit, cors, requestId,
      tracing, logger                     |       tracing, logger
                                          |
  src/routes/                             |   src/routes/
    + books.ts        (CRUD bookstore)    |     + health.ts   (GET /healthz)
    + authors.ts      (relationships)     |
    + reviews.ts      (rating validation) |
    + health.ts       (GET /healthz)      |
                                          |
  src/schemas/                            |   src/schemas/
    + book.ts, author.ts, review.ts       |     (empty add your own)
                                          |
  tests/                                  |   tests/
    + books.test.ts, authors.test.ts      |     + health.test.ts
                                          |
  openapi.json on /openapi.json           |   openapi.json on /openapi.json
  Swagger UI on /docs                     |   Swagger UI on /docs

The bookstore demo exists to show every contract-first pattern at once
(validation, relationships, errors, pagination, testing). When you've read
it twice, --minimal strips it so you can start your own app cleanly.
bookstore demo on the left · --minimal on the right

The five runtime templates

DaloyJS is runtime-portable, and the scaffolder is where that promise becomes a directory you can cd into. The buildApp() in src/app.ts is byte-identical across every template — the only thing that changes is the entrypoint file that hands a Request to that app:

TEMPLATES.md
bash
# Same app shape, five entry files.

node-basic/src/index.ts `serve()` from @daloyjs/core/node
bun-basic/src/index.ts           → Bun.serve(handle.url) with `bun --hot`
deno-basic/main.ts               → Deno.serve(handle.url) with deno.json tasks
cloudflare-worker/src/index.ts   → export default { fetch: handle.fetch }
vercel-edge/api/[...path].ts     → export const config = { runtime: "edge" }

The buildApp() in src/app.ts is byte-identical across all five templates.
That is on purpose — and it's the same property the "Same App on Five
Runtimes" post explored. Pick the one that matches where you deploy today;
you can copy the buildApp() to a different template tomorrow without
touching a single route or schema file.
same buildApp() · five different entrypoints

node-basic

--template node-basic

Default. Long-lived Node process, classic REST API shape, Hey API codegen wired in. Pick this when you're deploying to a normal container, a VM, or a Node-on-rails platform.

serve(app) — from @daloyjs/core/node

bun-basic

--template bun-basic

Bun-native. Uses bun --hot for dev, bun test for testing, and the same buildApp() handed to Bun.serve(). Fast cold start, identical handler code.

Bun.serve({ fetch: handle.url })

deno-basic

--template deno-basic

Deno-native. deno.json with tasks, npm: imports for @daloyjs/core, no node_modules. The only template with no package.json — which is exactly the point.

Deno.serve(handle.url)

cloudflare-worker

--template cloudflare-worker

Workers entrypoint with wrangler dev / wrangler deploy scripts and an env-typed config. Pairs with the KV session store from the sessions post.

export default { fetch: handle.fetch }

vercel-edge

--template vercel-edge

Catch-all api/[...path].ts that delegates to a single buildApp(). Comes with a short migration note covering Vercel's three handler shapes.

export const config = { runtime: "edge" }

The AGENTS.md drop-in (for the agent in the room)

One of the things I personally enjoy about the 2026 ecosystem is that everyproject also has, in practice, a coding agent looking at it — Copilot, Cursor, Claude Code, the JetBrains assistant, whatever you like. The scaffolder ships them their own briefing document so they don't have to guess at your conventions:

my-api/ · file tree
bash
# my-api/ (relevant excerpts)

AGENTS.md                              # short, opinionated, agent-facing
.agents/
  skills/
    daloyjs-best-practices/
      SKILL.md                         # the full workflow (~600 lines)
src/
  app.ts                               # buildApp() with secure defaults
  routes/
    health.ts                          # GET /healthz
.github/
  copilot-instructions.md              # points at AGENTS.md
  workflows/
    ci.yml          codeql.yml         deploy.yml
    zizmor.yml      scorecard.yml      vuln-scan.yml
    container-scan.yml                 dast.yml
  dependabot.yml
  CODEOWNERS
SECURITY.md
.npmrc                                 # ignore-scripts, minimum-release-age, ...
pnpm-workspace.yaml                    # blockExoticSubdeps, strictDepBuilds, ...
package.json
AGENTS.md is for the agent · README.md is for the human

AGENTS.mdis short and opinionated — it's the two-page summary every agent should read first. The real meat is in .agents/skills/daloyjs-best-practices/SKILL.md, which is the full ~600-line workflow doc: how to add a route, schema conventions, the testing recipe, security defaults, deployment notes per runtime. An agent that follows it produces code that looks like the rest of the codebase — which is the only kind of agent output that ages well.

my-api/AGENTS.md
bash
# AGENTS.md (excerpt)

A [DaloyJS](https://daloyjs.dev) Node.js REST API. **Contract-first**:
every route declares its method, path, body, params, and response schemas
inline; OpenAPI, validation, types, and a typed fetch SDK fall out of that
single declaration. Do not introduce parallel schema sources.

## Quick rules

1. Routes live in src/routes/<resource>.ts and register themselves on the
   shared `app`.
2. Schemas are JSON Schema objects, NOT zod (zod stays on the frontend if
   you want it).
3. Every mutating route requires a session OR an explicit `csrf({ ... })`
   bypass with a reason.
4. Throw typed errors (NotFoundError, BadRequestError, ...) from
   @daloyjs/core never return raw error responses.

For the full workflow adding routes step-by-step, schema conventions,
testing patterns, security guidance, and deployment notes read
.agents/skills/daloyjs-best-practices/SKILL.md.
short · opinionated · agent-facing

printStartupBanner() — small but important

The boring truth about devtools is that the moment you trust them is the moment they tell you something useful within a second of starting. printStartupBanner() is a zero-dependency helper in @daloyjs/core that replaces the inevitable console.log("listening on...") with something every scaffolded template uses:

src/index.ts
ts
// src/index.ts — what pnpm dev actually prints
import { serve } from "@daloyjs/core/node";
import { printStartupBanner } from "@daloyjs/core";
import { buildApp } from "./app.js";

const app = buildApp();
const { url } = await serve(app, { port: Number(process.env.PORT ?? 3000) });

printStartupBanner({
  name: "my-api",
  version: process.env.npm_package_version,
  url,
  runtime: `Node.js ${process.version}`,
  links: [
    { label: "Docs",   url: `${url}/docs` },
    { label: "OpenAPI", url: `${url}/openapi.json` },
    { label: "Health", url: `${url}/healthz` },
  ],
});
auto-detects color · ASCII fallback · works in CI
~/code/my-api · pnpm dev
bash
╭───────────────────────────────────────────────────────────╮
  my-api v0.1.0  · Node.js v20.18.0

  Local    http://localhost:3000
  Docs     http://localhost:3000/docs
  OpenAPI  http://localhost:3000/openapi.json
  Health   http://localhost:3000/healthz
╰───────────────────────────────────────────────────────────╯

# Adapts to your terminal automatically:
#   NO_COLOR=1 strips ANSI
#   DALOY_ASCII=1 (or non-UTF8 LANG) falls back to ASCII glyphs
#   Non-TTY (CI, piping to a file) drops the colors
that's the thing you'll see at 9:03am every weekday

Auto-detects TTY + NO_COLOR + FORCE_COLOR, falls back to ASCII glyphs in non-UTF-8 terminals, looks like a log line in CI. You can't see this kind of polish in a screenshot, but you feel it every morning.

--with-ci: the production-day-zero bundle

Every flag in create-daloyhas a default I'd argue for in a code review. --with-ci defaults to yes. That's the application-safe supply-chain-hardening posture from yesterday's post (Supply-Chain Hardening for TypeScript Libraries) dropped into your repo without you typing a single workflow line. The library publish workflow stays out because this scaffold is an app, not an npm package release train:

.github/ · scaffolded contents
bash
# pnpm create daloy@latest my-api --with-ci
# adds, on top of the application files:

.github/
  workflows/
    ci.yml            # pnpm install --frozen-lockfile --ignore-scripts; typecheck; test; verify lockfile
    codeql.yml        # TS/JS static analysis
    deploy.yml        # manual-only app deployment starter
    scorecard.yml     # weekly OpenSSF Scorecard
    zizmor.yml        # workflow lint on every push/PR
    vuln-scan.yml     # checks for known vulnerabilities
    container-scan.yml # runs Trivy scans on your dockerfile
  dependabot.yml      # weekly bumps, grouped per ecosystem
  CODEOWNERS          # assigns ownership to the repo owner 
SECURITY.md           # disclosure policy + supported versions
scripts/
  verify-lockfile-sources.mjs # catches non-npm registry / git / tarball drift
--with-ci · the app-safe posture in one flag

Pass --code-owner @your-team/security and CODEOWNERS gets that team on every workflow file — small detail, big payoff the first time someone tries to PR a change to ci.yml or deploy.yml.

Where to go next

You now have a running server, a contract, security middleware, and a CI pipeline. The next sixty seconds is the typed-client handoff — running pnpm gen, importing the SDK in your frontend, getting compile-time errors when your route changes shape. That's the entire subject of Contract-First Without the Codegen Dance. Read it next.

If you'd rather see how the same buildApp() deploys to all five runtimes — and what the differences feel like in practice — the Same App on Five Runtimes post is the other natural follow-up. The scaffolder docs have the full template reference and the full flag list, in case you want to grep for something specific.

Thanks for reading. The most flattering thing you can do is run pnpm create daloy@latest right now and tell me what felt awkward. Friction logs are the only kind of feedback I actually act on.

— Devlin