create-nwire 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/__tests__/scaffold.test.d.ts +8 -3
  2. package/dist/__tests__/scaffold.test.d.ts.map +1 -1
  3. package/dist/__tests__/scaffold.test.js +49 -22
  4. package/dist/__tests__/scaffold.test.js.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +22 -8
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/templates/L1/README.md +34 -3
  10. package/templates/L1/__tests__/hello.test.ts +54 -0
  11. package/templates/L1/app/api.ts +17 -13
  12. package/templates/L1/app/main.ts +11 -1
  13. package/templates/L1/app/routes/hello.ts +29 -0
  14. package/templates/L1/tsconfig.json +1 -1
  15. package/templates/L2/README.md +33 -13
  16. package/templates/L2/__tests__/todo-api.test.ts +115 -0
  17. package/templates/L2/app/api.ts +20 -115
  18. package/templates/L2/app/{errors.ts → errors/todo-errors.ts} +6 -6
  19. package/templates/L2/app/main.ts +7 -7
  20. package/templates/L2/app/middleware/require-user.ts +29 -0
  21. package/templates/L2/app/{model.ts → resources/todo.ts} +21 -5
  22. package/templates/L2/app/routes/complete-todo.ts +36 -0
  23. package/templates/L2/app/routes/create-todo.ts +27 -0
  24. package/templates/L2/app/routes/delete-todo.ts +31 -0
  25. package/templates/L2/app/routes/list-todos.ts +28 -0
  26. package/templates/L2/app/{store.ts → store/todo-store.ts} +13 -5
  27. package/templates/L2/tsconfig.json +1 -1
  28. package/templates/L4/README.md +67 -17
  29. package/templates/L4/__tests__/auto-moderate.test.ts +96 -0
  30. package/templates/L4/__tests__/submit-flow.test.ts +80 -0
  31. package/templates/L4/app/api.ts +19 -109
  32. package/templates/L4/app/app.ts +4 -4
  33. package/templates/L4/app/main.ts +11 -12
  34. package/templates/L4/modules/posts/actions/approve-post.ts +28 -0
  35. package/templates/L4/modules/posts/actions/reject-post.ts +29 -0
  36. package/templates/L4/modules/posts/actions/submit-post.ts +36 -0
  37. package/templates/L4/modules/posts/events/post-was-approved.ts +22 -0
  38. package/templates/L4/modules/posts/events/post-was-rejected.ts +20 -0
  39. package/templates/L4/modules/posts/events/post-was-submitted.ts +27 -0
  40. package/templates/L4/modules/posts/posts.module.ts +35 -0
  41. package/templates/L4/{app/queue.projection.ts → modules/posts/projections/queue-dashboard.ts} +18 -30
  42. package/templates/L4/modules/posts/routes/approve-post.ts +26 -0
  43. package/templates/L4/modules/posts/routes/get-post.ts +20 -0
  44. package/templates/L4/modules/posts/routes/list-queue.ts +22 -0
  45. package/templates/L4/modules/posts/routes/reject-post.ts +29 -0
  46. package/templates/L4/modules/posts/routes/submit-post.ts +28 -0
  47. package/templates/L4/{app/auto-moderate.workflow.ts → modules/posts/workflows/auto-moderate.ts} +13 -20
  48. package/templates/L4/tsconfig.json +1 -1
  49. package/templates/L2/app/middleware.ts +0 -32
  50. package/templates/L4/app/actions.ts +0 -81
  51. package/templates/L4/app/events.ts +0 -53
  52. package/templates/L4/app/moderation.module.ts +0 -28
@@ -1,9 +1,14 @@
1
1
  /**
2
2
  * Scaffold tests — verify each template materializes a coherent project
3
3
  * directory with the project name substituted and the canonical files
4
- * present. Does NOT install or boot the project (npm I in CI burns minutes
5
- * and adds nothing — the templates themselves are exercised by the
6
- * end-to-end examples that share their source).
4
+ * present.
5
+ *
6
+ * Every template is folder-organized: a flat-file shape would be smaller
7
+ * but graduating L1→L2→L4 with new conventions per level is hostile to
8
+ * users. One shape, one set of mental models.
9
+ *
10
+ * Does NOT install or boot the project (npm install in CI burns minutes
11
+ * and adds nothing — the boot path is exercised by the e2e examples).
7
12
  */
8
13
  export {};
9
14
  //# sourceMappingURL=scaffold.test.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scaffold.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
1
+ {"version":3,"file":"scaffold.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scaffold.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
@@ -1,9 +1,14 @@
1
1
  /**
2
2
  * Scaffold tests — verify each template materializes a coherent project
3
3
  * directory with the project name substituted and the canonical files
4
- * present. Does NOT install or boot the project (npm I in CI burns minutes
5
- * and adds nothing — the templates themselves are exercised by the
6
- * end-to-end examples that share their source).
4
+ * present.
5
+ *
6
+ * Every template is folder-organized: a flat-file shape would be smaller
7
+ * but graduating L1→L2→L4 with new conventions per level is hostile to
8
+ * users. One shape, one set of mental models.
9
+ *
10
+ * Does NOT install or boot the project (npm install in CI burns minutes
11
+ * and adds nothing — the boot path is exercised by the e2e examples).
7
12
  */
8
13
  import { mkdtempSync, readFileSync, existsSync, rmSync } from "node:fs";
9
14
  import { tmpdir } from "node:os";
@@ -20,48 +25,70 @@ describe("create-nwire scaffold", () => {
20
25
  if (existsSync(target))
21
26
  rmSync(target, { recursive: true, force: true });
22
27
  });
23
- it("L1 — writes minimal http+endpoint shape with project name swapped", () => {
28
+ it("L1 — minimal http+endpoint with folder-organized routes", () => {
24
29
  const { written } = scaffold({ target, template: "L1", name: "alpha" });
25
30
  expect(written.length).toBeGreaterThan(0);
26
31
  const pkg = JSON.parse(readFileSync(join(target, "package.json"), "utf8"));
27
32
  expect(pkg.name).toBe("alpha");
28
33
  expect(pkg.dependencies["@nwire/endpoint"]).toBeDefined();
29
34
  expect(pkg.dependencies["@nwire/http"]).toBeDefined();
30
- expect(existsSync(join(target, "src/main.ts"))).toBe(true);
31
- expect(existsSync(join(target, "src/api.ts"))).toBe(true);
35
+ // Folder shape: app/main.ts + app/api.ts + app/routes/<route>.ts + __tests__/
36
+ expect(existsSync(join(target, "app/main.ts"))).toBe(true);
37
+ expect(existsSync(join(target, "app/api.ts"))).toBe(true);
38
+ expect(existsSync(join(target, "app/routes/hello.ts"))).toBe(true);
39
+ expect(existsSync(join(target, "__tests__/hello.test.ts"))).toBe(true);
32
40
  expect(existsSync(join(target, "tsconfig.json"))).toBe(true);
33
41
  expect(existsSync(join(target, ".gitignore"))).toBe(true);
34
- expect(readFileSync(join(target, "src/main.ts"), "utf8")).toContain('"alpha"');
42
+ expect(readFileSync(join(target, "app/main.ts"), "utf8")).toContain('"alpha"');
35
43
  });
36
- it("L2 — writes the full service shape (model + errors + middleware + store)", () => {
44
+ it("L2 — service shape with resources/errors/middleware/store/routes folders", () => {
37
45
  const { written } = scaffold({ target, template: "L2", name: "beta-svc" });
38
46
  expect(written.length).toBeGreaterThan(0);
39
47
  const pkg = JSON.parse(readFileSync(join(target, "package.json"), "utf8"));
40
48
  expect(pkg.name).toBe("beta-svc");
41
49
  expect(pkg.dependencies["@nwire/forge"]).toBeDefined();
42
- for (const f of ["main.ts", "api.ts", "model.ts", "errors.ts", "middleware.ts", "store.ts"]) {
43
- expect(existsSync(join(target, "src", f))).toBe(true);
50
+ // Boot files
51
+ for (const f of ["app/main.ts", "app/api.ts"]) {
52
+ expect(existsSync(join(target, f))).toBe(true);
44
53
  }
54
+ // Folder-organized pieces
55
+ expect(existsSync(join(target, "app/resources/todo.ts"))).toBe(true);
56
+ expect(existsSync(join(target, "app/errors/todo-errors.ts"))).toBe(true);
57
+ expect(existsSync(join(target, "app/middleware/require-user.ts"))).toBe(true);
58
+ expect(existsSync(join(target, "app/store/todo-store.ts"))).toBe(true);
59
+ // One route per file
60
+ for (const r of ["list-todos", "create-todo", "complete-todo", "delete-todo"]) {
61
+ expect(existsSync(join(target, `app/routes/${r}.ts`))).toBe(true);
62
+ }
63
+ expect(existsSync(join(target, "__tests__/todo-api.test.ts"))).toBe(true);
45
64
  });
46
- it("L4 — writes the enterprise shape (modules + workflow + projection)", () => {
65
+ it("L4 — enterprise shape with modules/<bc>/{events,actions,workflows,projections,routes}", () => {
47
66
  const { written } = scaffold({ target, template: "L4", name: "gamma-app" });
48
67
  expect(written.length).toBeGreaterThan(0);
49
68
  const pkg = JSON.parse(readFileSync(join(target, "package.json"), "utf8"));
50
69
  expect(pkg.name).toBe("gamma-app");
51
70
  expect(pkg.dependencies["@nwire/app"]).toBeDefined();
52
71
  expect(pkg.dependencies["@nwire/forge"]).toBeDefined();
53
- for (const f of [
54
- "main.ts",
55
- "app.ts",
56
- "api.ts",
57
- "events.ts",
58
- "actions.ts",
59
- "moderation.module.ts",
60
- "auto-moderate.workflow.ts",
61
- "queue.projection.ts",
62
- ]) {
63
- expect(existsSync(join(target, "src", f))).toBe(true);
72
+ // Boot trio in app/
73
+ for (const f of ["app/main.ts", "app/api.ts", "app/app.ts"]) {
74
+ expect(existsSync(join(target, f))).toBe(true);
75
+ }
76
+ // The posts bounded context
77
+ expect(existsSync(join(target, "modules/posts/posts.module.ts"))).toBe(true);
78
+ for (const e of ["post-was-submitted", "post-was-approved", "post-was-rejected"]) {
79
+ expect(existsSync(join(target, `modules/posts/events/${e}.ts`))).toBe(true);
80
+ }
81
+ for (const a of ["submit-post", "approve-post", "reject-post"]) {
82
+ expect(existsSync(join(target, `modules/posts/actions/${a}.ts`))).toBe(true);
83
+ }
84
+ expect(existsSync(join(target, "modules/posts/workflows/auto-moderate.ts"))).toBe(true);
85
+ expect(existsSync(join(target, "modules/posts/projections/queue-dashboard.ts"))).toBe(true);
86
+ for (const r of ["submit-post", "approve-post", "reject-post", "list-queue", "get-post"]) {
87
+ expect(existsSync(join(target, `modules/posts/routes/${r}.ts`))).toBe(true);
64
88
  }
89
+ // Tests
90
+ expect(existsSync(join(target, "__tests__/submit-flow.test.ts"))).toBe(true);
91
+ expect(existsSync(join(target, "__tests__/auto-moderate.test.ts"))).toBe(true);
65
92
  });
66
93
  it("refuses to scaffold into a non-empty target", () => {
67
94
  const { written } = scaffold({ target, template: "L1", name: "first" });
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.test.js","sourceRoot":"","sources":["../../src/__tests__/scaffold.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scaffold.test.js","sourceRoot":"","sources":["../../src/__tests__/scaffold.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAUH,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAiB/C,UAAU,eAAe;IACvB,MAAM,EAAI,MAAM,CAAC;IACjB,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,CAAC,EAAK,MAAM,CAAC;CAClB;AAeD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG;IAAE,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAiBrE;AAqGD,wBAAsB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAE5C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAkBH,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAiB/C,UAAU,eAAe;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAeD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG;IAAE,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAiBrE;AAoHD,wBAAsB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAE5C"}
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * no template fetching from GitHub. Picking a level is the only decision;
13
13
  * everything else is whatever the user does once their starter is live.
14
14
  */
15
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
15
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
16
16
  import { dirname, join, resolve } from "node:path";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { defineCommand, runMain } from "citty";
@@ -21,15 +21,15 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
21
21
  const TEMPLATES = {
22
22
  L1: {
23
23
  title: "Minimal (L1)",
24
- blurb: "httpInterface + endpoint only. ~30 LOC. No app, no DI, no domain.",
24
+ blurb: "httpInterface + endpoint. Folder-organized routes/. No app, no DI.",
25
25
  },
26
26
  L2: {
27
27
  title: "Service (L2)",
28
- blurb: "Standalone http app with structured errors, middleware, in-memory store.",
28
+ blurb: "Per-file routes/resources/errors/middleware/store. In-memory CRUD.",
29
29
  },
30
30
  L4: {
31
31
  title: "Enterprise (L4)",
32
- blurb: "Modules, actors, workflows, projections, resolvers — full forge shape.",
32
+ blurb: "modules/<bc>/{events,actions,workflows,projections,routes}. Full forge shape.",
33
33
  },
34
34
  };
35
35
  /**
@@ -64,13 +64,27 @@ export function scaffold(opts) {
64
64
  applyVariables(opts.target, { PROJECT_NAME: projectName });
65
65
  return { written };
66
66
  }
67
+ /**
68
+ * Files that get a leading underscore in the template tree because
69
+ * npm strips dotfiles when packing tarballs. We rename them back to
70
+ * their canonical dotfile names during scaffold.
71
+ *
72
+ * The earlier blanket rule "any entry starting with `_` becomes `.`"
73
+ * was wrong — it mangled directories like `__tests__` into `._tests__`.
74
+ * Keep the rename explicit so new template files don't surprise us.
75
+ */
76
+ const UNDERSCORE_TO_DOT = {
77
+ _gitignore: ".gitignore",
78
+ _npmrc: ".npmrc",
79
+ _eslintrc: ".eslintrc",
80
+ _eslintignore: ".eslintignore",
81
+ _prettierrc: ".prettierrc",
82
+ _env: ".env",
83
+ };
67
84
  function copyTree(src, dest, log) {
68
85
  for (const entry of readdirSync(src)) {
69
86
  const s = join(src, entry);
70
- // npm strips dotfiles from published tarballs, so templates ship them
71
- // as `_gitignore` / `_npmrc`; rename back on copy. Same trick that
72
- // create-vite uses.
73
- const renamed = entry.startsWith("_") ? "." + entry.slice(1) : entry;
87
+ const renamed = UNDERSCORE_TO_DOT[entry] ?? entry;
74
88
  const d = join(dest, renamed);
75
89
  if (statSync(s).isDirectory()) {
76
90
  mkdirSync(d, { recursive: true });
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nwire",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "description": "Scaffolder for new Nwire projects. Run `pnpm create nwire <name>` or `npm create nwire <name>` to bootstrap.",
5
5
  "keywords": [
6
6
  "nwire",
@@ -3,6 +3,9 @@
3
3
  The smallest possible Nwire shape — one POST route, one inline handler,
4
4
  no app, no DI, no domain primitives.
5
5
 
6
+ Despite being tiny, the project is **folder-organized** so it matches L2
7
+ and L4. Adding route #2 is a single new file, not a rewrite.
8
+
6
9
  ## Run
7
10
 
8
11
  ```bash
@@ -18,17 +21,45 @@ curl -X POST http://localhost:3000/hello \
18
21
  -d '{"name":"Alice"}'
19
22
  ```
20
23
 
24
+ ## Test
25
+
26
+ ```bash
27
+ pnpm test
28
+ ```
29
+
30
+ ## Folder shape
31
+
32
+ ```
33
+ {{PROJECT_NAME}}/
34
+ ├── app/
35
+ │ ├── main.ts ← endpoint().serve(api).run()
36
+ │ ├── api.ts ← httpInterface().wire(route, handler)
37
+ │ └── routes/
38
+ │ └── hello.ts ← POST /hello — route + handler pair
39
+ └── __tests__/
40
+ └── hello.test.ts
41
+ ```
42
+
43
+ ## Where to add a …
44
+
45
+ | You want to add … | Where it goes |
46
+ | ----------------- | --------------------------------------------------------------- |
47
+ | A new route | `app/routes/<verb-noun>.ts` exports `{name}Route + {name}Handler`; `.wire()` it from `app/api.ts` |
48
+ | Middleware | Inline in `app/api.ts` via `.use(middlewareFn)` |
49
+ | Container deps | Graduate to L2 — `app/store/` + `.provide(container)` in api.ts |
50
+
21
51
  ## What you get free
22
52
 
23
53
  | Feature | Comes from |
24
54
  | -------------------------------------- | ----------------- |
25
55
  | Zod request validation | `@nwire/http` |
26
56
  | OpenAPI schema (visit `/openapi.json`) | `@nwire/http` |
57
+ | Scalar UI docs (visit `/docs`) | `@nwire/http` |
27
58
  | Graceful SIGTERM drain | `@nwire/endpoint` |
28
59
  | K8s probes on port 9400 | `@nwire/endpoint` |
29
60
 
30
61
  ## Grow up
31
62
 
32
- When you outgrow inline handlers (multiple routes, persistence, events)
33
- re-scaffold with `pnpm create nwire <name> --template L2` for the
34
- service shape or `--template L4` for the full enterprise shape.
63
+ When you outgrow inline handlers (multiple routes, persistence, container
64
+ bindings, errors) re-scaffold with `pnpm create nwire <name> --template L2`
65
+ for the service shape or `--template L4` for the full enterprise shape.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Smoke test — POST /hello returns the personalized greeting, and an
3
+ * empty `name` is rejected with a structured validation error.
4
+ *
5
+ * The test boots `api` (no full endpoint — no port binding) and dispatches
6
+ * raw fetch requests against `api.compile()` mounted on a Node http server.
7
+ * That's the same boot path as production, just without the lightship
8
+ * probes and SIGTERM dance.
9
+ */
10
+
11
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
12
+ import http from "node:http";
13
+ import type { Server } from "node:http";
14
+ import { api } from "../app/api";
15
+
16
+ let server: Server;
17
+ let url: string;
18
+
19
+ beforeAll(async () => {
20
+ server = http.createServer(api.compile());
21
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
22
+ const addr = server.address() as { port: number };
23
+ url = `http://127.0.0.1:${addr.port}`;
24
+ });
25
+
26
+ afterAll(async () => {
27
+ await new Promise<void>((resolve, reject) =>
28
+ server.close((err) => (err ? reject(err) : resolve())),
29
+ );
30
+ });
31
+
32
+ describe("hello route", () => {
33
+ it("returns the personalized greeting", async () => {
34
+ const res = await fetch(`${url}/hello`, {
35
+ method: "POST",
36
+ headers: { "content-type": "application/json" },
37
+ body: JSON.stringify({ name: "Alice" }),
38
+ });
39
+ expect(res.status).toBe(200);
40
+ const body = (await res.json()) as { message: string };
41
+ expect(body.message).toBe("Hello, Alice!");
42
+ });
43
+
44
+ it("rejects an empty name with a structured validation error", async () => {
45
+ const res = await fetch(`${url}/hello`, {
46
+ method: "POST",
47
+ headers: { "content-type": "application/json" },
48
+ body: JSON.stringify({ name: "" }),
49
+ });
50
+ expect(res.status).toBe(400);
51
+ const body = (await res.json()) as { error: { code: string } };
52
+ expect(body.error.code).toBe("validation_failed");
53
+ });
54
+ });
@@ -1,18 +1,22 @@
1
1
  /**
2
- * Single routeZod-validated body, typed handler return.
2
+ * HTTP interfacethe surface this app exposes.
3
3
  *
4
- * This is the smallest possible Nwire shape. Once you outgrow inline
5
- * handlers (multiple bounded contexts, persistence, events) graduate
6
- * to the L2 or L4 templates.
4
+ * The interface is built by wiring route bindings to handlers. Each route
5
+ * lives under `./routes/` and exports a `{name}Route` + `{name}Handler`
6
+ * pair. The interface stays a 1-line-per-route reducer; new routes drop in
7
+ * with a single import + a single `.wire(...)` call.
8
+ *
9
+ * Why a folder of routes for "just one route"?
10
+ *
11
+ * - Adding route #2 is a single new file, not a rewrite of api.ts.
12
+ * - Each route's verb + path + schema + handler live together.
13
+ * - The pattern matches L2 + L4, so going up a level is mechanical.
14
+ *
15
+ * When you outgrow inline handlers (multiple bounded contexts, domain
16
+ * events, persistence) graduate via `pnpm create nwire <name> --template L2`.
7
17
  */
8
18
 
9
- import { httpInterface, post } from "@nwire/http";
10
- import { z } from "zod";
19
+ import { httpInterface } from "@nwire/http";
20
+ import { helloRoute, helloHandler } from "./routes/hello";
11
21
 
12
- export const api = httpInterface().wire(
13
- post("/hello", { body: z.object({ name: z.string().min(1) }) }),
14
- async ({ input }) => {
15
- const { name } = input as { name: string };
16
- return { message: `Hello, ${name}!` };
17
- },
18
- );
22
+ export const api = httpInterface().wire(helloRoute, helloHandler);
@@ -1,5 +1,15 @@
1
1
  /**
2
- * Entry — boots the http interface inside an endpoint.
2
+ * Entry point boot the HTTP interface inside an endpoint.
3
+ *
4
+ * The lifecycle:
5
+ *
6
+ * 1. `api` (./api.ts) is a built interface — routes registered, schemas
7
+ * compiled, ready to serve.
8
+ * 2. `endpoint(name, { port }).serve(api).run()` starts the HTTP listener
9
+ * AND wires graceful SIGTERM drain + K8s readiness/liveness probes.
10
+ *
11
+ * No createApp, no DI container, no domain primitives. Two packages,
12
+ * three files of source code. The smallest possible Nwire shape.
3
13
  *
4
14
  * Run: pnpm dev
5
15
  * Try: curl -X POST http://localhost:3000/hello \
@@ -0,0 +1,29 @@
1
+ /**
2
+ * POST /hello — Zod-validated body, typed handler return.
3
+ *
4
+ * The shape every route file follows:
5
+ *
6
+ * - `{name}Route` — the `post(path, schema)` binding
7
+ * - `{name}Handler` — the `async ({ input }) => ...` handler
8
+ *
9
+ * `api.ts` wires them with `.wire(helloRoute, helloHandler)`.
10
+ *
11
+ * The framework converts the Zod schema into OpenAPI on boot — visit
12
+ * `/openapi.json` or the Scalar UI at `/docs` to see it.
13
+ *
14
+ * To add a route, copy this file, rename, then `.wire()` the new pair
15
+ * from `api.ts`.
16
+ */
17
+
18
+ import { post, type HttpHandler } from "@nwire/http";
19
+ import { z } from "zod";
20
+
21
+ const HelloBody = z.object({
22
+ name: z.string().min(1).describe("Caller's first name"),
23
+ });
24
+
25
+ export const helloRoute = post("/hello", { body: HelloBody });
26
+
27
+ export const helloHandler: HttpHandler<z.infer<typeof HelloBody>> = async ({ input }) => {
28
+ return { message: `Hello, ${input.name}!` };
29
+ };
@@ -11,5 +11,5 @@
11
11
  "noEmit": true,
12
12
  "types": ["node", "vite/client"]
13
13
  },
14
- "include": ["app/**/*"]
14
+ "include": ["app/**/*", "__tests__/**/*"]
15
15
  }
@@ -30,19 +30,39 @@ curl -X POST http://localhost:3000/api/todos/<id>/complete \
30
30
  -H "x-user-id: alice"
31
31
  ```
32
32
 
33
- ## Source map
34
-
35
- | File | What it does |
36
- | --------------- | ----------------------------------------------------------- |
37
- | `main.ts` | Boots `endpoint().serve(api).run()` |
38
- | `api.ts` | The 4 routes + inline handlers |
39
- | `model.ts` | `defineResource("Todo", ...)` — public field list |
40
- | `errors.ts` | `defineError(...)` — `TodoNotFound`, `TodoAlreadyCompleted` |
41
- | `middleware.ts` | `requireUser` checks `x-user-id` header |
42
- | `store.ts` | In-memory `TodoStore` + plugin shape |
43
-
44
- Swap `store.ts` for `@nwire/data-drizzle` (Postgres) or
45
- `@nwire/store-mongo` once you need durability.
33
+ ## Folder shape
34
+
35
+ ```
36
+ {{PROJECT_NAME}}/
37
+ ├── app/
38
+ │ ├── main.ts endpoint().serve(api).run()
39
+ │ ├── api.ts httpInterface().use(...).wire(...)
40
+ │ ├── resources/
41
+ │ │ └── todo.ts defineResourcepublic field list + project()
42
+ │ ├── errors/
43
+ │ │ └── todo-errors.ts ← defineError instances
44
+ │ ├── middleware/
45
+ │ │ └── require-user.ts ← requireUser middleware
46
+ │ ├── store/
47
+ │ │ └── todo-store.ts ← in-memory TodoStore + plugin shape
48
+ │ └── routes/
49
+ │ ├── list-todos.ts ← GET /api/todos
50
+ │ ├── create-todo.ts ← POST /api/todos
51
+ │ ├── complete-todo.ts ← POST /api/todos/:id/complete
52
+ │ └── delete-todo.ts ← DELETE /api/todos/:id
53
+ └── __tests__/
54
+ └── todo-api.test.ts
55
+ ```
56
+
57
+ ## Where to add a …
58
+
59
+ | You want to add … | Where it goes |
60
+ | ----------------- | ----------------------------------------------------------------------------- |
61
+ | A new route | `app/routes/<verb-noun>.ts` exports `{name}Route + {name}Handler`; `.wire()` it from `api.ts` |
62
+ | A new error type | `app/errors/<resource>-errors.ts` — `defineError({ code, status, summary })` |
63
+ | A new middleware | `app/middleware/<name>.ts` exports `HttpMiddleware`; `.use()` it from `api.ts`|
64
+ | A new resource | `app/resources/<name>.ts` — `defineResource` schema + `public` field list |
65
+ | Real persistence | Swap `app/store/todo-store.ts` for `@nwire/data-drizzle` or `@nwire/store-mongo` |
46
66
 
47
67
  ## Grow up
48
68
 
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Smoke test — the four CRUD operations + the requireUser middleware.
3
+ *
4
+ * Boots the api against a real Node http server, registers a fresh
5
+ * in-memory TodoStore on a container, and dispatches fetch requests
6
+ * exactly the way clients will.
7
+ */
8
+
9
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
10
+ import http from "node:http";
11
+ import type { Server } from "node:http";
12
+ import { InMemoryContainer } from "@nwire/container";
13
+ import { TodoStore } from "../app/store/todo-store";
14
+ import { api } from "../app/api";
15
+
16
+ let server: Server;
17
+ let url: string;
18
+
19
+ beforeAll(async () => {
20
+ const container = new InMemoryContainer();
21
+ container.register("todos", new TodoStore());
22
+ api.provide(container);
23
+
24
+ server = http.createServer(api.compile());
25
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
26
+ const addr = server.address() as { port: number };
27
+ url = `http://127.0.0.1:${addr.port}`;
28
+ });
29
+
30
+ afterAll(async () => {
31
+ await new Promise<void>((resolve, reject) =>
32
+ server.close((err) => (err ? reject(err) : resolve())),
33
+ );
34
+ });
35
+
36
+ async function req(
37
+ path: string,
38
+ init: RequestInit & { user?: string } = {},
39
+ ): Promise<{ status: number; body: unknown }> {
40
+ const headers: Record<string, string> = {
41
+ "content-type": "application/json",
42
+ ...((init.headers as Record<string, string>) ?? {}),
43
+ };
44
+ if (init.user) headers["x-user-id"] = init.user;
45
+ const res = await fetch(`${url}${path}`, { ...init, headers });
46
+ const text = await res.text();
47
+ let body: unknown = text;
48
+ try {
49
+ body = JSON.parse(text);
50
+ } catch {
51
+ /* keep as text */
52
+ }
53
+ return { status: res.status, body };
54
+ }
55
+
56
+ describe("todo api", () => {
57
+ it("requires x-user-id (requireUser middleware)", async () => {
58
+ const r = await req("/api/todos", { method: "GET" });
59
+ expect(r.status).toBe(401);
60
+ expect((r.body as { error: { code: string } }).error.code).toBe("MISSING_USER_ID");
61
+ });
62
+
63
+ it("creates, lists, completes, deletes", async () => {
64
+ // create
65
+ const created = await req("/api/todos", {
66
+ method: "POST",
67
+ body: JSON.stringify({ text: "buy milk" }),
68
+ user: "alice",
69
+ });
70
+ expect(created.status).toBe(201);
71
+ const todo = created.body as { id: string; text: string; status: string };
72
+ expect(todo.text).toBe("buy milk");
73
+ expect(todo.status).toBe("open");
74
+
75
+ // list
76
+ const listed = await req("/api/todos", { method: "GET", user: "alice" });
77
+ expect(listed.status).toBe(200);
78
+ const list = listed.body as { items: Array<{ id: string }>; total: number };
79
+ expect(list.total).toBe(1);
80
+ expect(list.items[0]!.id).toBe(todo.id);
81
+
82
+ // complete
83
+ const completed = await req(`/api/todos/${todo.id}/complete`, {
84
+ method: "POST",
85
+ user: "alice",
86
+ });
87
+ expect(completed.status).toBe(200);
88
+ expect((completed.body as { status: string }).status).toBe("completed");
89
+
90
+ // double-complete → 409
91
+ const again = await req(`/api/todos/${todo.id}/complete`, {
92
+ method: "POST",
93
+ user: "alice",
94
+ });
95
+ expect(again.status).toBe(409);
96
+
97
+ // delete
98
+ const removed = await req(`/api/todos/${todo.id}`, {
99
+ method: "DELETE",
100
+ user: "alice",
101
+ });
102
+ expect(removed.status).toBe(204);
103
+ });
104
+
105
+ it("isolates todos per user (alice doesn't see bob's)", async () => {
106
+ await req("/api/todos", {
107
+ method: "POST",
108
+ body: JSON.stringify({ text: "bob's task" }),
109
+ user: "bob",
110
+ });
111
+ const aliceList = await req("/api/todos", { method: "GET", user: "alice" });
112
+ const items = (aliceList.body as { items: Array<{ text: string }> }).items;
113
+ expect(items.find((i) => i.text === "bob's task")).toBeUndefined();
114
+ });
115
+ });