Skip to content

Commit 1e6c787

Browse files
docs(examples): add nuxt 4 + nitro 3 recipes, target node 24
Two new examples joining the existing four: examples/nuxt/ - Nuxt 4 App Router + Nitro server routes under `server/api/`. - `server/utils/db.ts` auto-imported into every handler. - `server/plugins/close-pool.ts` hooks Nitro's `close` event to end the shared pg Pool on HMR reload + graceful shutdown — without it, every dev save leaks a connection and production eventually hits "too many clients". - `multiTenant({ strict: true })` wired via a `dbFor(tid)` factory; AbortSignal forwarded from `event.node.req.signal` to sumak's `exec({ signal })`. examples/nitro/ - Standalone Nitro 3 (no Nuxt layer on top) — same file-system routing, same auto-imports, smaller surface area. - Demonstrates `db.transaction()` for a monotonic per-source sequence counter where two racing producers would otherwise collide. - README covers deploy presets (Node / Cloudflare / Lambda / Vercel) since "write once, deploy anywhere" is Nitro's whole pitch. All six examples now: - target **Node 24+** via `engines.node: ">=24"`. - drop `--experimental-strip-types` — Node 24 runs `.ts` natively. - run against the latest library versions (Next 16, pg 8.20, Express 5.2, Fastify 5.8, Nuxt 4, Nitro 3). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0b37358 commit 1e6c787

26 files changed

Lines changed: 478 additions & 10 deletions

examples/README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
Minimal integration recipes — not tutorials. Each example is a single directory you can copy into a new project and adapt. They're small on purpose: the point is to show how sumak plugs into a given runtime, not to ship a production app.
44

5-
| directory | stack | what it demonstrates |
6-
| ----------------------------- | ------------------------------ | ------------------------------------------------------ |
7-
| [`express/`](./express) | Express 5 + pg | Request-scoped sumak, transactions, streaming response |
8-
| [`fastify/`](./fastify) | Fastify 5 + pg | Plugin registration, schema validation + sumak types |
9-
| [`aws-lambda/`](./aws-lambda) | AWS Lambda + RDS Data API / pg | Cold-start-safe client reuse, AbortSignal on timeout |
10-
| [`nextjs/`](./nextjs) | Next.js 15 App Router + pg | Server Actions, streaming, per-request tenant scoping |
5+
All examples target **Node 24+**.
6+
7+
| directory | stack | what it demonstrates |
8+
| ----------------------------- | ------------------------------ | --------------------------------------------------------------------------- |
9+
| [`express/`](./express) | Express 5 + pg | Request-scoped sumak, transactions, streaming response |
10+
| [`fastify/`](./fastify) | Fastify 5 + pg | Plugin registration, schema validation + sumak types |
11+
| [`aws-lambda/`](./aws-lambda) | AWS Lambda + RDS Data API / pg | Cold-start-safe client reuse, AbortSignal on timeout |
12+
| [`nextjs/`](./nextjs) | Next.js 16 App Router + pg | Server Actions, streaming, per-request tenant scoping |
13+
| [`nuxt/`](./nuxt) | Nuxt 4 + Nitro + pg | File-system routes, multi-tenant plugin, HMR-safe Pool via Nitro close hook |
14+
| [`nitro/`](./nitro) | Nitro 3 standalone + pg | No Nuxt layer — deploy to Node / Cloudflare / Lambda from the same source |
1115

1216
## Running locally
1317

examples/aws-lambda/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@
66
"dependencies": {
77
"pg": "^8.20.0",
88
"sumak": "*"
9+
},
10+
"engines": {
11+
"node": ">=24"
912
}
1013
}

examples/express/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
"private": true,
55
"type": "module",
66
"scripts": {
7-
"dev": "node --experimental-strip-types --watch --env-file=.env src/server.ts",
7+
"dev": "node --watch --env-file=.env src/server.ts",
88
"migrate": "sumak migrate up",
99
"migrate:plan": "sumak migrate plan"
1010
},
1111
"dependencies": {
1212
"express": "^5.2.1",
1313
"pg": "^8.20.0",
1414
"sumak": "*"
15+
},
16+
"engines": {
17+
"node": ">=24"
1518
}
1619
}

examples/fastify/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
"private": true,
55
"type": "module",
66
"scripts": {
7-
"dev": "node --experimental-strip-types --watch --env-file=.env src/server.ts",
7+
"dev": "node --watch --env-file=.env src/server.ts",
88
"migrate": "sumak migrate up"
99
},
1010
"dependencies": {
1111
"fastify": "^5.8.5",
1212
"pg": "^8.20.0",
1313
"sumak": "*"
14+
},
15+
"engines": {
16+
"node": ">=24"
1417
}
1518
}

examples/nextjs/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# sumak + Next.js 15
1+
# sumak + Next.js 16
22

3-
Next.js App Router + Server Components + Server Actions with a multi-tenant scope.
3+
Next.js 16 App Router + Server Components + Server Actions with a multi-tenant scope.
44

55
## What it shows
66

examples/nextjs/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@
1414
"react": "^19.2.5",
1515
"react-dom": "^19.2.5",
1616
"sumak": "*"
17+
},
18+
"engines": {
19+
"node": ">=24"
1720
}
1821
}

examples/nitro/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# sumak + Nitro 3
2+
3+
Standalone Nitro server — no Nuxt on top. Same runtime Nuxt uses internally, but you get to write the whole server surface directly: file-system routes, auto-imported utils, typed event handlers, and deploy presets for Node / Deno / Bun / Cloudflare Workers / AWS Lambda / Vercel.
4+
5+
## What it shows
6+
7+
- **`utils/db.ts` is auto-imported everywhere.** Nitro lifts every export under `utils/` into the global scope of route handlers — no `import { db } from "../../utils/db.ts"` noise. The Pool sits on `globalThis.__pgPool` so HMR doesn't duplicate it.
8+
- **`plugins/close-pool.ts` hooks Nitro's `close` event.** Every dev-server reload and every production graceful shutdown triggers `close`; we end the shared Pool there. Without this hook, HMR leaks connections on every save and Postgres eventually refuses new clients.
9+
- **File-system method dispatch.** `events.get.ts` / `events.post.ts` — the suffix drives the HTTP method, no manual `if (event.method === ...)`. Less boilerplate, same types.
10+
- **Transactional sequence counter.** The POST handler reads `MAX(seq)` and inserts the new row inside a single `db.transaction(async tx => ...)` — both statements share a connection, so two concurrent producers can't hand out the same sequence number.
11+
- **Typed query + runtime config.** `useRuntimeConfig().databaseUrl` reads from env at runtime; sumak's `tables` record drives the typed builder; schema lives in `utils/schema.ts` so both route handlers and `sumak migrate` share it.
12+
13+
## Structure
14+
15+
```
16+
nitro.config.ts — compatibilityDate + runtime config
17+
utils/
18+
schema.ts — shared table definitions (auto-imported)
19+
db.ts — Pool + sumak singleton (auto-imported)
20+
plugins/
21+
close-pool.ts — ends the Pool on Nitro `close` (HMR + shutdown)
22+
routes/
23+
api/
24+
events.get.ts — list / cursor-paginate
25+
events.post.ts — ingest with monotonic seq
26+
```
27+
28+
## Run
29+
30+
```bash
31+
export DATABASE_URL="postgres://postgres:pg@localhost:5432/postgres"
32+
pnpm install
33+
pnpm migrate
34+
pnpm dev
35+
```
36+
37+
```bash
38+
curl -X POST http://localhost:3000/api/events \
39+
-H 'content-type: application/json' \
40+
-d '{"source":"checkout","payload":{"orderId":42}}'
41+
curl http://localhost:3000/api/events?limit=10
42+
```
43+
44+
## Deploy
45+
46+
```bash
47+
# Node (default preset)
48+
pnpm build
49+
node .output/server/index.mjs
50+
51+
# Cloudflare Workers
52+
NITRO_PRESET=cloudflare_module pnpm build
53+
54+
# AWS Lambda
55+
NITRO_PRESET=aws-lambda pnpm build
56+
```
57+
58+
Pick the preset that matches your target. sumak's pg driver runs on any of them; on edge runtimes you may want to swap to an HTTP-based driver (e.g. Neon's serverless driver) since long-lived pg connections don't fit the Workers connection model — but that's your call, not sumak's.

examples/nitro/nitro.config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Standalone Nitro server — no Nuxt layer on top. File-system
2+
// routes under `routes/` and `api/`, auto-imports under `utils/`,
3+
// server-only plugins under `plugins/`. Deploys to Node, Deno,
4+
// Bun, Cloudflare Workers, AWS Lambda, Vercel edge — same code.
5+
6+
export default defineNitroConfig({
7+
compatibilityDate: "2025-07-01",
8+
runtimeConfig: {
9+
databaseUrl: "",
10+
},
11+
})

examples/nitro/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "sumak-example-nitro",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"build": "nitro build",
8+
"dev": "nitro dev",
9+
"preview": "node .output/server/index.mjs",
10+
"migrate": "sumak migrate up"
11+
},
12+
"dependencies": {
13+
"nitro": "^3.0.0",
14+
"pg": "^8.20.0",
15+
"sumak": "*"
16+
},
17+
"engines": {
18+
"node": ">=24"
19+
}
20+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Release the shared pg Pool when Nitro shuts down — including dev-
2+
// server HMR reloads. Without it, each reload orphans the old
3+
// module's Pool rather than closing it, and Postgres eventually
4+
// refuses new connections with "too many clients".
5+
//
6+
// Re-read `globalThis.__pgPool` at close time rather than importing
7+
// from `utils/db.ts` so a stale module graph can't pin the wrong
8+
// reference.
9+
export default defineNitroPlugin((nitro) => {
10+
nitro.hooks.hook("close", async () => {
11+
const g = globalThis as unknown as { __pgPool?: { end(): Promise<void> } }
12+
if (g.__pgPool) {
13+
await g.__pgPool.end()
14+
g.__pgPool = undefined
15+
}
16+
})
17+
})

0 commit comments

Comments
 (0)