RouteGraph v1.0.0 is out. It's stable, it's typed, it's not going to rewrite your framework of choice into a decorator soup. → Try it in 90 seconds

Quickstart

You’ll have a validated, documented, hot-reloading REST API before your coffee finishes brewing. Realistically before it’s done pouring.

Prerequisites: Node.js 18+, and a package manager. We show pnpm below because that’s what the monorepo itself uses — npm and bun work identically.

Option A — the CLI does it for you

~/
$ npx @routegraph/cli init —name my-api —yes
◈ scaffolding my-api…
✓ routes/health/GET.ts
✓ routes/users/GET.ts, POST.ts
✓ routes/users/[id]/GET.ts, DELETE.ts
✓ package.json, tsconfig.json, index.ts
Done. cd my-api && pnpm install && pnpm dev

routegraph init runs an interactive wizard by default — project name, adapter, port, features, package manager. Pass --yes to skip straight to the defaults (Express, port 3000, every feature, pnpm).

~/my-api
$ cd my-api && pnpm install && pnpm dev
◈ RouteGraph Dev Server
API → http://localhost:3000/api
Docs → http://localhost:3000/_routegraph
Adapter express
Routes 5 registered
Watch enabled
GET /health
GET /users
POST /users
GET /users/:id
DELETE /users/:id

Option B — wire it up yourself

Install

terminal
pnpm add @routegraph/core @routegraph/express zod
pnpm add -D @routegraph/cli @routegraph/watcher @routegraph/docs

There is no unscoped routegraph package — always install @routegraph/core plus the adapter for your framework. zod is a peer dependency everywhere.

Write your first route

routes/users/[id]/GET.ts
import { z } from 'zod'
import type { RouteConfig, RouteHandler } from '@routegraph/core'
 
export const config = {
  description: 'Get a single user by their ID',
  tags: ['users'],
  request: {
    params: z.object({ id: z.string() }),
  },
  response: {
    200: z.object({ id: z.string(), name: z.string(), email: z.string().email() }),
    404: z.object({ error: z.string() }),
  },
} satisfies RouteConfig
 
const handler: RouteHandler<typeof config> = async (req, res) => {
  // req.params.id is typed as `string` — inferred from `config.request.params`
  const user = findUserById(req.params.id)
  if (!user) return res.status(404).json({ error: 'User not found' })
  res.status(200).json(user)
}
 
export default handler

config is optional. export default handler alone is a valid, unvalidated route — add schemas whenever you’re ready for validation, inferred types, and free docs.

Boot it

index.ts
import express from 'express'
import { RouteGraph } from '@routegraph/core'
import { createExpressRouter } from '@routegraph/express'
import { createDocsMiddleware } from '@routegraph/docs'
 
const graph = new RouteGraph({ routesDir: './routes' })
await graph.load()
 
const app = express()
app.use(express.json())
app.use('/api', createExpressRouter(graph))
app.use('/_routegraph', createDocsMiddleware(graph))
app.listen(3000)

Call it

terminal
curl http://localhost:3000/api/users/1

Then open http://localhost:3000/_routegraph — every route you just wrote, rendered into a searchable, try-it-out UI. You didn’t write a line of docs.

What you get, immediately

GET /healthscaffolded for you, always green
Validation400 on bad input, before your handler runs
Docslive at /_routegraph, generated from the same Zod schemas
Typesinferred end to end, no .d.ts to hand-maintain
Hot reloadadd a route file mid-pnpm dev, it’s live

That’s the entire quickstart. There is no part two where we admit you also need a router.config.js.