create-katajs 0.1.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/index.js +568 -0
  4. package/dist/templates/api/.github/workflows/deploy.yml +26 -0
  5. package/dist/templates/api/README.md +95 -0
  6. package/dist/templates/api/_dev.vars.example +3 -0
  7. package/dist/templates/api/_gitignore +11 -0
  8. package/dist/templates/api/drizzle.config.ts +10 -0
  9. package/dist/templates/api/package.json +32 -0
  10. package/dist/templates/api/scripts/graph.ts +19 -0
  11. package/dist/templates/api/scripts/modules.ts +7 -0
  12. package/dist/templates/api/src/app.ts +39 -0
  13. package/dist/templates/api/src/db/schema.ts +11 -0
  14. package/dist/templates/api/src/index.ts +6 -0
  15. package/dist/templates/api/src/modules/posts/index.ts +31 -0
  16. package/dist/templates/api/src/modules/posts/posts.errors.ts +15 -0
  17. package/dist/templates/api/src/modules/posts/posts.repository.ts +22 -0
  18. package/dist/templates/api/src/modules/posts/posts.routes.ts +20 -0
  19. package/dist/templates/api/src/modules/posts/posts.schema.ts +12 -0
  20. package/dist/templates/api/src/modules/posts/posts.service.ts +26 -0
  21. package/dist/templates/api/src/types.d.ts +31 -0
  22. package/dist/templates/api/tsconfig.json +22 -0
  23. package/dist/templates/api/wrangler.jsonc +35 -0
  24. package/dist/templates/auth-snippets/api/src/db/auth-schema.ts +53 -0
  25. package/dist/templates/auth-snippets/api/src/modules/auth/auth.config.ts +30 -0
  26. package/dist/templates/auth-snippets/api/src/modules/auth/auth.errors.ts +10 -0
  27. package/dist/templates/auth-snippets/api/src/modules/auth/auth.middleware.ts +37 -0
  28. package/dist/templates/auth-snippets/api/src/modules/auth/auth.routes.ts +7 -0
  29. package/dist/templates/auth-snippets/api/src/modules/auth/index.ts +28 -0
  30. package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/auth.errors.ts +10 -0
  31. package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/auth.middleware.ts +37 -0
  32. package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/auth.routes.ts +7 -0
  33. package/dist/templates/auth-snippets/monorepo/apps/api/src/modules/auth/index.ts +29 -0
  34. package/dist/templates/auth-snippets/monorepo/packages/auth/package.json +22 -0
  35. package/dist/templates/auth-snippets/monorepo/packages/auth/src/auth.ts +40 -0
  36. package/dist/templates/auth-snippets/monorepo/packages/auth/src/index.ts +2 -0
  37. package/dist/templates/auth-snippets/monorepo/packages/auth/tsconfig.json +4 -0
  38. package/dist/templates/auth-snippets/monorepo/packages/db/src/auth-schema.ts +53 -0
  39. package/dist/templates/monorepo/.github/workflows/deploy.yml +39 -0
  40. package/dist/templates/monorepo/README.md +91 -0
  41. package/dist/templates/monorepo/_gitignore +12 -0
  42. package/dist/templates/monorepo/apps/api/_dev.vars.example +7 -0
  43. package/dist/templates/monorepo/apps/api/_gitignore +7 -0
  44. package/dist/templates/monorepo/apps/api/package.json +36 -0
  45. package/dist/templates/monorepo/apps/api/scripts/graph.ts +19 -0
  46. package/dist/templates/monorepo/apps/api/scripts/modules.ts +7 -0
  47. package/dist/templates/monorepo/apps/api/src/app.ts +42 -0
  48. package/dist/templates/monorepo/apps/api/src/index.ts +7 -0
  49. package/dist/templates/monorepo/apps/api/src/modules/posts/index.ts +31 -0
  50. package/dist/templates/monorepo/apps/api/src/modules/posts/posts.errors.ts +15 -0
  51. package/dist/templates/monorepo/apps/api/src/modules/posts/posts.repository.ts +22 -0
  52. package/dist/templates/monorepo/apps/api/src/modules/posts/posts.routes.ts +20 -0
  53. package/dist/templates/monorepo/apps/api/src/modules/posts/posts.schema.ts +13 -0
  54. package/dist/templates/monorepo/apps/api/src/modules/posts/posts.service.ts +26 -0
  55. package/dist/templates/monorepo/apps/api/src/types.d.ts +29 -0
  56. package/dist/templates/monorepo/apps/api/tsconfig.json +7 -0
  57. package/dist/templates/monorepo/apps/api/wrangler.jsonc +34 -0
  58. package/dist/templates/monorepo/package.json +24 -0
  59. package/dist/templates/monorepo/packages/api-client/package.json +20 -0
  60. package/dist/templates/monorepo/packages/api-client/src/index.ts +43 -0
  61. package/dist/templates/monorepo/packages/api-client/tsconfig.json +4 -0
  62. package/dist/templates/monorepo/packages/db/drizzle.config.ts +12 -0
  63. package/dist/templates/monorepo/packages/db/package.json +26 -0
  64. package/dist/templates/monorepo/packages/db/src/index.ts +13 -0
  65. package/dist/templates/monorepo/packages/db/src/schema.ts +12 -0
  66. package/dist/templates/monorepo/packages/db/tsconfig.json +4 -0
  67. package/dist/templates/monorepo/pnpm-workspace.yaml +3 -0
  68. package/dist/templates/monorepo/tsconfig.base.json +18 -0
  69. package/dist/templates/monorepo/turbo.json +19 -0
  70. package/dist/templates/monorepo-worker/apps/worker/_dev.vars.example +4 -0
  71. package/dist/templates/monorepo-worker/apps/worker/_gitignore +6 -0
  72. package/dist/templates/monorepo-worker/apps/worker/package.json +29 -0
  73. package/dist/templates/monorepo-worker/apps/worker/src/app.ts +27 -0
  74. package/dist/templates/monorepo-worker/apps/worker/src/index.ts +6 -0
  75. package/dist/templates/monorepo-worker/apps/worker/src/modules/example-consumer/example.consumer.ts +23 -0
  76. package/dist/templates/monorepo-worker/apps/worker/src/modules/example-consumer/example.service.ts +18 -0
  77. package/dist/templates/monorepo-worker/apps/worker/src/modules/example-consumer/index.ts +24 -0
  78. package/dist/templates/monorepo-worker/apps/worker/src/types.d.ts +19 -0
  79. package/dist/templates/monorepo-worker/apps/worker/tsconfig.json +7 -0
  80. package/dist/templates/monorepo-worker/apps/worker/wrangler.jsonc +28 -0
  81. package/package.json +54 -0
@@ -0,0 +1,37 @@
1
+ import { defineMiddleware } from '@katajs/core';
2
+ import type { AuthInstance } from '@{{PROJECT_NAME}}/auth';
3
+ import { UnauthorizedError } from './auth.errors';
4
+
5
+ type SessionUser = {
6
+ id: string;
7
+ email: string;
8
+ name: string;
9
+ };
10
+
11
+ declare module 'hono' {
12
+ interface ContextVariableMap {
13
+ user: SessionUser;
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Attach the current user (if any) to `c.var.user`. Routes that require
19
+ * auth should use `requireAuthMiddleware` instead.
20
+ */
21
+ export const attachUserMiddleware = (auth: AuthInstance) =>
22
+ defineMiddleware(async (c, next) => {
23
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
24
+ if (session?.user) {
25
+ c.set('user', session.user as SessionUser);
26
+ }
27
+ await next();
28
+ });
29
+
30
+ /** Require an authenticated user — throws `UnauthorizedError` if absent. */
31
+ export const requireAuthMiddleware = (auth: AuthInstance) =>
32
+ defineMiddleware(async (c, next) => {
33
+ const session = await auth.api.getSession({ headers: c.req.raw.headers });
34
+ if (!session?.user) throw new UnauthorizedError();
35
+ c.set('user', session.user as SessionUser);
36
+ await next();
37
+ });
@@ -0,0 +1,7 @@
1
+ import { Hono } from 'hono';
2
+ import type { AppEnv } from '../../app';
3
+
4
+ export const authRoutes = new Hono<AppEnv>().all('/*', async (c) => {
5
+ const auth = c.var.container.resolve('authInstance');
6
+ return auth.handler(c.req.raw);
7
+ });
@@ -0,0 +1,29 @@
1
+ import { defineModule } from '@katajs/core';
2
+ import * as schema from '@{{PROJECT_NAME}}/db';
3
+ import type { DrizzleClient } from '@katajs/drizzle';
4
+ import { createAuth, type AuthInstance } from '@{{PROJECT_NAME}}/auth';
5
+ import { authRoutes } from './auth.routes';
6
+
7
+ export const authModule = defineModule({
8
+ name: 'auth',
9
+ provides: {
10
+ authInstance: (c): AuthInstance =>
11
+ createAuth(
12
+ c.db as DrizzleClient<typeof schema>,
13
+ c.env as { BETTER_AUTH_SECRET?: string; BETTER_AUTH_URL?: string },
14
+ ),
15
+ },
16
+ requires: [] as const,
17
+ routes: authRoutes,
18
+ prefix: '/auth',
19
+ // katajs:module-consumer
20
+ });
21
+
22
+ /** Services this module contributes to the container's `Registry`. */
23
+ export type AuthRegistry = {
24
+ authInstance: AuthInstance;
25
+ };
26
+
27
+ export type { AuthInstance } from '@{{PROJECT_NAME}}/auth';
28
+ export { UnauthorizedError } from './auth.errors';
29
+ export { attachUserMiddleware, requireAuthMiddleware } from './auth.middleware';
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@{{PROJECT_NAME}}/auth",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts"
10
+ },
11
+ "scripts": {
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "dependencies": {
15
+ "@{{PROJECT_NAME}}/db": "workspace:*",
16
+ "@katajs/drizzle": "^0.1.0",
17
+ "better-auth": "^1.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "^5.6.3"
21
+ }
22
+ }
@@ -0,0 +1,40 @@
1
+ import { betterAuth } from 'better-auth';
2
+ import { drizzleAdapter as betterAuthDrizzleAdapter } from 'better-auth/adapters/drizzle';
3
+ import type { DrizzleClient } from '@katajs/drizzle';
4
+ import * as schema from '@{{PROJECT_NAME}}/db';
5
+
6
+ /**
7
+ * Build a Better Auth instance bound to a request-scoped Drizzle client.
8
+ * Called per-request from the auth module's `provides` factory.
9
+ *
10
+ * Note: Better Auth has its *own* `drizzleAdapter` (imported as
11
+ * `betterAuthDrizzleAdapter`) — distinct from `@katajs/drizzle`'s
12
+ * `drizzleAdapter`. The naming overlap is unavoidable.
13
+ */
14
+ export function createAuth(
15
+ db: DrizzleClient<typeof schema>,
16
+ env: { BETTER_AUTH_SECRET?: string; BETTER_AUTH_URL?: string },
17
+ ) {
18
+ return betterAuth({
19
+ database: betterAuthDrizzleAdapter(db, {
20
+ provider: 'pg',
21
+ schema: {
22
+ user: schema.user,
23
+ session: schema.session,
24
+ account: schema.account,
25
+ verification: schema.verification,
26
+ },
27
+ }),
28
+ secret: env.BETTER_AUTH_SECRET,
29
+ baseURL: env.BETTER_AUTH_URL,
30
+ emailAndPassword: { enabled: true },
31
+ });
32
+ }
33
+
34
+ export type AuthInstance = ReturnType<typeof createAuth>;
35
+
36
+ export type SessionUser = {
37
+ id: string;
38
+ email: string;
39
+ name: string;
40
+ };
@@ -0,0 +1,2 @@
1
+ export { createAuth } from './auth';
2
+ export type { AuthInstance, SessionUser } from './auth';
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "include": ["src/**/*"]
4
+ }
@@ -0,0 +1,53 @@
1
+ import { boolean, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
2
+
3
+ /**
4
+ * Better Auth tables. The exact column shape is dictated by Better Auth's
5
+ * Drizzle adapter — keep these in sync with `better-auth` releases. Run
6
+ * `pnpm db:generate` (from the workspace root) after changes.
7
+ */
8
+ export const user = pgTable('user', {
9
+ id: text('id').primaryKey(),
10
+ name: text('name').notNull(),
11
+ email: text('email').notNull().unique(),
12
+ emailVerified: boolean('email_verified').notNull().default(false),
13
+ image: text('image'),
14
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
15
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
16
+ });
17
+
18
+ export const session = pgTable('session', {
19
+ id: text('id').primaryKey(),
20
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
21
+ token: text('token').notNull().unique(),
22
+ ipAddress: text('ip_address'),
23
+ userAgent: text('user_agent'),
24
+ userId: text('user_id')
25
+ .notNull()
26
+ .references(() => user.id, { onDelete: 'cascade' }),
27
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
28
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
29
+ });
30
+
31
+ export const account = pgTable('account', {
32
+ id: text('id').primaryKey(),
33
+ accountId: text('account_id').notNull(),
34
+ providerId: text('provider_id').notNull(),
35
+ userId: text('user_id')
36
+ .notNull()
37
+ .references(() => user.id, { onDelete: 'cascade' }),
38
+ accessToken: text('access_token'),
39
+ refreshToken: text('refresh_token'),
40
+ idToken: text('id_token'),
41
+ password: text('password'),
42
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
43
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
44
+ });
45
+
46
+ export const verification = pgTable('verification', {
47
+ id: text('id').primaryKey(),
48
+ identifier: text('identifier').notNull(),
49
+ value: text('value').notNull(),
50
+ expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
51
+ createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
52
+ updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
53
+ });
@@ -0,0 +1,39 @@
1
+ name: Deploy
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ concurrency:
9
+ group: deploy-${{ github.ref }}
10
+ cancel-in-progress: false
11
+
12
+ jobs:
13
+ deploy:
14
+ name: Typecheck + deploy apps/api
15
+ runs-on: ubuntu-latest
16
+ timeout-minutes: 10
17
+
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+
21
+ - uses: pnpm/action-setup@v4
22
+ with:
23
+ version: 9
24
+
25
+ - uses: actions/setup-node@v4
26
+ with:
27
+ node-version: 20
28
+ cache: pnpm
29
+
30
+ - name: Install dependencies
31
+ run: pnpm install --frozen-lockfile
32
+
33
+ - name: Typecheck
34
+ run: pnpm typecheck
35
+
36
+ - name: Deploy
37
+ run: pnpm --filter @{{PROJECT_NAME}}/api deploy
38
+ env:
39
+ CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
@@ -0,0 +1,91 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ A katajs monorepo. One Hono+Workers API, a typed RPC client, and a shared Drizzle schema package.
4
+
5
+ ## Layout
6
+
7
+ ```
8
+ {{PROJECT_NAME}}/
9
+ ├── apps/
10
+ │ └── api/ ← Hono + Cloudflare Workers app (deploys to CF)
11
+ ├── packages/
12
+ │ ├── db/ ← Drizzle schema + migrations (shared)
13
+ │ └── api-client/ ← typed Hono RPC client (export AppType + factory)
14
+ ├── pnpm-workspace.yaml
15
+ ├── turbo.json
16
+ └── package.json ← delegating scripts
17
+ ```
18
+
19
+ Why this layout: schema and client types are shared across the workspace; deployable units (the API today, queue workers tomorrow) live in `apps/`. Both consume the packages via `workspace:*` protocol.
20
+
21
+ ## Quickstart
22
+
23
+ ```bash
24
+ # Install all workspace deps
25
+ pnpm install
26
+
27
+ # Generate migration SQL from the Drizzle schema
28
+ pnpm db:generate
29
+
30
+ # Apply migrations to your local Postgres (set up your .dev.vars first)
31
+ pnpm db:migrate
32
+
33
+ # Run the API in dev (Wrangler watches packages/db too)
34
+ pnpm dev
35
+ ```
36
+
37
+ `http://localhost:8787/health` should return `{ ok: true }` once the API is up.
38
+
39
+ ## Common commands
40
+
41
+ | Command | What it does |
42
+ |---|---|
43
+ | `pnpm dev` | `turbo run dev` — runs `wrangler dev` in `apps/api` |
44
+ | `pnpm typecheck` | typechecks every package in parallel (cached) |
45
+ | `pnpm test` | runs vitest in every package that has tests |
46
+ | `pnpm graph` | regenerates `apps/api/graph.html` from the module graph |
47
+ | `pnpm db:generate` | drizzle-kit generate in `packages/db` |
48
+ | `pnpm db:migrate` | drizzle-kit migrate against `packages/db/drizzle.config.ts` |
49
+ | `pnpm deploy` | `wrangler deploy` in `apps/api` |
50
+
51
+ ## Adding modules and services
52
+
53
+ The katajs CLI works inside the API:
54
+
55
+ ```bash
56
+ cd apps/api
57
+ pnpm katajs add module billing
58
+ pnpm katajs add service archived --in posts
59
+ pnpm katajs add route delete /:id --in users
60
+ ```
61
+
62
+ Or from the root, the CLI walks down to find the API project automatically (planned for Phase 2).
63
+
64
+ ## Using the API client elsewhere
65
+
66
+ When you build a frontend (TanStack Start, Next.js, etc.) or a separate worker that calls the API, depend on `@{{PROJECT_NAME}}/api-client`:
67
+
68
+ ```ts
69
+ import { createApiClient } from '@{{PROJECT_NAME}}/api-client';
70
+ import type { AppType } from '@{{PROJECT_NAME}}/api';
71
+
72
+ const api = createApiClient<AppType>('https://api.example.com');
73
+ const res = await api.posts.$post({ json: { title: 't', body: 'b' } });
74
+ const data = await res.json(); // typed end-to-end
75
+ ```
76
+
77
+ The client is generic over the API's `AppType`, so the package itself has zero workspace coupling — the consumer brings its own `AppType`. This is also the home for future cross-cutting client concerns (auth header injection, retry policy, error parsing).
78
+
79
+ ## What's where
80
+
81
+ - **Drizzle schema** lives in `packages/db/src/schema.ts`. All apps import it from `@{{PROJECT_NAME}}/db`.
82
+ - **Migration SQL** lives in `packages/db/migrations/` (generated by `drizzle-kit`).
83
+ - **Drizzle config** lives in `packages/db/drizzle.config.ts`. Reads connection string from env.
84
+ - **Modules** live in `apps/api/src/modules/`.
85
+ - **Hono app + `createApp({...})`** lives in `apps/api/src/app.ts`.
86
+
87
+ ## Deploy
88
+
89
+ GitHub Actions workflow at `.github/workflows/deploy.yml` typechecks and deploys `apps/api` on push to `main`. You'll need a `CLOUDFLARE_API_TOKEN` secret with permissions to deploy your Worker.
90
+
91
+ For migrations, run `pnpm db:migrate` against your production database from your CI or a one-shot local command — it's not currently part of the deploy workflow.
@@ -0,0 +1,12 @@
1
+ node_modules/
2
+ dist/
3
+ .turbo/
4
+ .wrangler/
5
+ .dev.vars
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ *.log
10
+ .DS_Store
11
+ coverage/
12
+ *.tsbuildinfo
@@ -0,0 +1,7 @@
1
+ # Local-only secrets for `wrangler dev`. Copy this file to `.dev.vars`
2
+ # and fill in real values. The `.dev.vars` file is gitignored.
3
+ #
4
+ # DATABASE_URL is read by drizzle-kit (run from packages/db) for migrations.
5
+ # Wrangler reads HYPERDRIVE.localConnectionString from wrangler.jsonc instead.
6
+
7
+ DATABASE_URL="postgres://postgres:postgres@localhost:5432/{{PROJECT_NAME}}_dev"
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ dist/
3
+ .wrangler/
4
+ .dev.vars
5
+ .turbo/
6
+ *.tsbuildinfo
7
+ graph.html
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@{{PROJECT_NAME}}/api",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts"
10
+ },
11
+ "scripts": {
12
+ "dev": "wrangler dev",
13
+ "deploy": "wrangler deploy",
14
+ "graph": "tsx scripts/graph.ts",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "dependencies": {
18
+ "@{{PROJECT_NAME}}/db": "workspace:*",
19
+ "@hono/zod-validator": "^0.4.1",
20
+ "@katajs/core": "^0.1.0",
21
+ "@katajs/drizzle": "^0.1.0",
22
+ "drizzle-orm": "^0.36.4",
23
+ "hono": "^4.6.10",
24
+ "pg": "^8.13.1",
25
+ "zod": "^3.23.8"
26
+ },
27
+ "devDependencies": {
28
+ "@cloudflare/workers-types": "^4.20241112.0",
29
+ "@katajs/cli": "^0.1.0",
30
+ "@types/node": "^22.9.0",
31
+ "@types/pg": "^8.11.10",
32
+ "tsx": "^4.19.2",
33
+ "typescript": "^5.6.3",
34
+ "wrangler": "^3.86.0"
35
+ }
36
+ }
@@ -0,0 +1,19 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { inspectModules } from '@katajs/core';
5
+
6
+ import { modules } from './modules';
7
+
8
+ const here = fileURLToPath(new URL('.', import.meta.url));
9
+ const out = resolve(here, '..', 'graph.html');
10
+
11
+ const insp = inspectModules(modules);
12
+ writeFileSync(out, insp.html({ title: '{{PROJECT_NAME}} — module graph' }));
13
+
14
+ // eslint-disable-next-line no-console
15
+ console.log(`✓ wrote ${out}`);
16
+ // eslint-disable-next-line no-console
17
+ console.log(
18
+ ` ${insp.modules.length} modules • ${insp.edges.length} dependencies • ${insp.routes.length} routes`,
19
+ );
@@ -0,0 +1,7 @@
1
+ import { postsModule } from '../src/modules/posts/index';
2
+ // katajs:graph-imports
3
+
4
+ export const modules = [
5
+ postsModule,
6
+ // katajs:graph-modules
7
+ ];
@@ -0,0 +1,42 @@
1
+ import { createApp, type RequestVariables } from '@katajs/core';
2
+ import { drizzleAdapter } from '@katajs/drizzle';
3
+ import * as schema from '@{{PROJECT_NAME}}/db';
4
+
5
+ import { postsModule } from './modules/posts/index';
6
+ // katajs:module-imports
7
+
8
+ export type Bindings = {
9
+ HYPERDRIVE: Hyperdrive;
10
+ };
11
+
12
+ export type AppEnv = {
13
+ Bindings: Bindings;
14
+ Variables: RequestVariables;
15
+ };
16
+
17
+ const { app, queue } = createApp({
18
+ bindings: {} as Bindings,
19
+ db: drizzleAdapter({ schema }),
20
+ modules: [
21
+ postsModule,
22
+ // katajs:modules
23
+ ],
24
+ // Producer manifest. Each entry surfaces a typed wrapper at
25
+ // c.var.queues.<name>.send(body), validated against the schema.
26
+ queues: {
27
+ // katajs:queues
28
+ },
29
+ // Define your HTTP surface here. Add `.get()` / `.post()` for ad-hoc routes
30
+ // (health checks, webhooks) and `.route(prefix, module.routes)` per module.
31
+ routes: (base) =>
32
+ base
33
+ .get('/health', (c) =>
34
+ c.json({ ok: true, requestId: c.var.requestId }),
35
+ )
36
+ .route(postsModule.prefix, postsModule.routes),
37
+ // katajs:routes
38
+ });
39
+
40
+ export { queue };
41
+ export default app;
42
+ export type AppType = typeof app;
@@ -0,0 +1,7 @@
1
+ import app, { queue } from './app';
2
+
3
+ // Worker default export. `queue` is undefined when no module declares a
4
+ // `consumer:` field; Cloudflare Workers handles either case fine.
5
+ export default { fetch: app.fetch, queue };
6
+
7
+ export type { AppType, AppEnv, Bindings } from './app';
@@ -0,0 +1,31 @@
1
+ import { defineModule } from '@katajs/core';
2
+ import { makePostRepository, type PostRepository } from './posts.repository';
3
+ import { makePostService, type PostsService } from './posts.service';
4
+ // katajs:module-service-imports
5
+ import { postsRoutes } from './posts.routes';
6
+
7
+ export const postsModule = defineModule({
8
+ name: 'posts',
9
+ provides: {
10
+ postRepository: (c): PostRepository => makePostRepository(c.db),
11
+ postService: (c): PostsService => makePostService(c),
12
+ // katajs:module-provides
13
+ },
14
+ requires: [] as const,
15
+ routes: postsRoutes,
16
+ prefix: '/posts',
17
+ // katajs:module-consumer
18
+ });
19
+
20
+ /**
21
+ * Services this module contributes to the container's `Registry`. Compose it
22
+ * in `app.ts` via `interface Registry extends PostsRegistry {}`. Adding a new
23
+ * module is one extra `extends` clause — no key-by-key bookkeeping.
24
+ */
25
+ export type PostsRegistry = {
26
+ postRepository: PostRepository;
27
+ postService: PostsService;
28
+ // katajs:module-registry
29
+ };
30
+
31
+ export type { PostRepository, PostsService };
@@ -0,0 +1,15 @@
1
+ import { AppError } from '@katajs/core';
2
+
3
+ export class PostNotFoundError extends AppError {
4
+ override readonly status = 404;
5
+ override readonly code = 'post_not_found';
6
+ override readonly publicMessage = 'Post not found';
7
+
8
+ constructor(public readonly postId: string) {
9
+ super(`Post ${postId} not found`);
10
+ }
11
+
12
+ override get publicPayload() {
13
+ return { postId: this.postId };
14
+ }
15
+ }
@@ -0,0 +1,22 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import type { DrizzleClientOrTx } from '@katajs/drizzle';
3
+ import { posts, type NewPost, type Post } from '@{{PROJECT_NAME}}/db';
4
+ import * as schema from '@{{PROJECT_NAME}}/db';
5
+
6
+ type Schema = typeof schema;
7
+
8
+ export function makePostRepository(dbOrTx: DrizzleClientOrTx<Schema>) {
9
+ return {
10
+ async findById(id: string): Promise<Post | undefined> {
11
+ const rows = await dbOrTx.select().from(posts).where(eq(posts.id, id)).limit(1);
12
+ return rows[0];
13
+ },
14
+ async insert(input: NewPost): Promise<Post> {
15
+ const [row] = await dbOrTx.insert(posts).values(input).returning();
16
+ if (!row) throw new Error('insert returned no row');
17
+ return row;
18
+ },
19
+ };
20
+ }
21
+
22
+ export type PostRepository = ReturnType<typeof makePostRepository>;
@@ -0,0 +1,20 @@
1
+ import { Hono } from 'hono';
2
+ import { validate } from '@katajs/core';
3
+ import type { AppEnv } from '../../app';
4
+ import { CreatePostSchema, PostIdParam } from './posts.schema';
5
+
6
+ export const postsRoutes = new Hono<AppEnv>()
7
+ .get('/:id', ...validate({ param: PostIdParam }), async (c) => {
8
+ const { id } = c.req.valid('param');
9
+ const service = c.var.resolve('postService');
10
+ const post = await service.getById(id);
11
+ return c.json({ post });
12
+ })
13
+ .post('/', ...validate({ body: CreatePostSchema }), async (c) => {
14
+ const input = c.req.valid('json');
15
+ const service = c.var.resolve('postService');
16
+ const post = await service.create(input);
17
+ return c.json({ post }, 201);
18
+ })
19
+ // katajs:module-routes
20
+ ;
@@ -0,0 +1,13 @@
1
+ import { z } from 'zod';
2
+
3
+ export const CreatePostSchema = z.object({
4
+ title: z.string().min(1).max(200),
5
+ body: z.string().min(1).max(10_000),
6
+ authorId: z.string().min(1),
7
+ });
8
+ export type CreatePostInput = z.infer<typeof CreatePostSchema>;
9
+
10
+ export const PostIdParam = z.object({
11
+ id: z.string().uuid(),
12
+ });
13
+ export type PostIdParamInput = z.infer<typeof PostIdParam>;
@@ -0,0 +1,26 @@
1
+ import type { RequestContainer } from '@katajs/core';
2
+ import { PostNotFoundError } from './posts.errors';
3
+ import type { CreatePostInput } from './posts.schema';
4
+ import type { Post } from '@{{PROJECT_NAME}}/db';
5
+
6
+ export type PostsService = {
7
+ getById(id: string): Promise<Post>;
8
+ create(input: CreatePostInput): Promise<Post>;
9
+ };
10
+
11
+ export function makePostService(c: RequestContainer): PostsService {
12
+ return {
13
+ async getById(id) {
14
+ const repo = c.resolve('postRepository');
15
+ const post = await repo.findById(id);
16
+ if (!post) throw new PostNotFoundError(id);
17
+ return post;
18
+ },
19
+ async create(input) {
20
+ return c.withTransaction(async (tx) => {
21
+ const repo = tx.resolve('postRepository');
22
+ return repo.insert(input);
23
+ });
24
+ },
25
+ };
26
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Module augmentations for `@katajs/core`. Picked up automatically by
3
+ * tsconfig's `include` glob — no runtime import needed. Co-locate everything
4
+ * that teaches the framework about this app's concrete types here.
5
+ *
6
+ * The `katajs:registry-imports` and `katajs:registry` markers below are used
7
+ * by `katajs add module` to insert new module registry entries automatically.
8
+ */
9
+ import type { DrizzleClient } from '@katajs/drizzle';
10
+ import type * as schema from '@{{PROJECT_NAME}}/db';
11
+ import type { Bindings } from './app';
12
+
13
+ import type { PostsRegistry } from './modules/posts/index';
14
+ // katajs:registry-imports
15
+
16
+ declare module '@katajs/core' {
17
+ interface AppDb extends DrizzleClient<typeof schema> {}
18
+ interface AppEnv extends Bindings {}
19
+ interface Registry
20
+ extends PostsRegistry
21
+ // katajs:registry
22
+ {}
23
+
24
+ // Typed producer queues. Each entry maps a queue name to its body type so
25
+ // c.var.queues.<name>.send(body) is fully typed.
26
+ interface QueuesRegistry {
27
+ // katajs:queues-registry
28
+ }
29
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "types": ["@cloudflare/workers-types", "node"]
5
+ },
6
+ "include": ["src/**/*", "scripts/**/*"]
7
+ }