@nilejs/cli 0.0.6 → 1.0.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.
package/README.md CHANGED
@@ -2,187 +2,112 @@
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/@nilejs/cli.svg)](https://www.npmjs.com/package/@nilejs/cli)
4
4
 
5
- CLI for scaffolding and generating [Nile](https://github.com/nile-js/nile/tree/main/packages/nile) backend projects.
5
+ Create [Nile Js](https://nile-js.github.io/nile/) projects and generate services, actions and schemas.
6
6
 
7
- ## Install
8
-
9
- ```bash
10
- bun add -g @nilejs/cli
11
- ```
12
-
13
- ```bash
14
- npm install -g @nilejs/cli
15
- ```
16
-
17
- Or use without installing:
7
+ ## Create a project
18
8
 
19
9
  ```bash
20
10
  bunx @nilejs/cli new my-app
11
+ cd my-app
12
+ bun install
13
+ cp .env.example .env
14
+ bun run dev
21
15
  ```
22
16
 
23
- ```bash
24
- npx @nilejs/cli new my-app
25
- ```
17
+ The server starts on `http://localhost:8000`. The starter stores its data in an embedded PGlite database, so there is no database server to install. The [getting started guide](https://nile-js.github.io/nile/guide/start/getting-started/) walks through the starter step by step.
26
18
 
27
- ## Commands
19
+ The starter includes:
28
20
 
29
- ### `nile new <project-name>`
21
+ - `src/index.ts`: server entry point on Bun's native HTTP server, with a logger and an `onBoot` schema push
22
+ - `src/db/`: PGlite and Drizzle client, the `tasks` table and a `taskModel` built with `createModel`
23
+ - `src/services/`: a `tasks` service with create, list, get, update and delete actions
24
+ - `Dockerfile`, `docker-compose.yml` and `docker-compose.dev.yml` for container deployments
25
+ - `drizzle.config.ts`, `.env.example`, `tsconfig.json` and `package.json`
30
26
 
31
- Scaffold a new Nile project. Copies the project template, replaces placeholders with your project name, and prints next steps.
27
+ ## Generate code
32
28
 
33
- ```bash
34
- nile new my-app
35
- ```
29
+ Run the generators from the project root. The starter's `gen` script calls the CLI for you.
36
30
 
37
- Output:
38
-
39
- ```
40
- Creating project: my-app
41
-
42
- Copying project files...
43
- Configuring project...
44
- Project "my-app" created.
45
-
46
- Next steps:
47
-
48
- cd my-app
49
- bun install
50
- cp .env.example .env
51
- bun run dev
52
- ```
53
-
54
- The scaffolded project includes:
55
-
56
- - `src/index.ts`, server entry with PGLite and Drizzle
57
- - `src/db/`, database client, schema, types, and a `tasks` model using `createModel`
58
- - `src/services/`, a `tasks` service with five CRUD actions
59
- - `drizzle.config.ts`, `.env.example`, `tsconfig.json`, `package.json`
60
-
61
- ### `nile generate service <name>`
31
+ | Command | Description |
32
+ |---|---|
33
+ | `bun run gen service <name>` | Add a service with a sample action |
34
+ | `bun run gen action <service> <name>` | Add an action to an existing service |
35
+ | `bun run gen action <service> <name> --folder` | Add an action in its own folder, for private helper modules |
36
+ | `bun run gen schema` | Generate Zod schemas and TypeScript types from your actions |
62
37
 
63
- Alias: `nile gen service <name>`
38
+ `generate` also has the alias `gen`, so `bunx @nilejs/cli gen service users` works outside the starter.
64
39
 
65
- Generate a new service directory under `src/services/` with a demo action and barrel export. Run this from the project root.
40
+ ### Services
66
41
 
67
42
  ```bash
68
- nile gen service users
43
+ bun run gen service users
69
44
  ```
70
45
 
71
46
  Creates:
72
47
 
73
- ```
48
+ ```text
74
49
  src/services/users/
75
- sample.ts # Demo action with Zod schema, handler, and createAction
76
- index.ts # Barrel export
50
+ actions/sample.ts Sample action with a schema, an input type and a named handler
51
+ index.ts Exports the sample action
77
52
  ```
78
53
 
79
- After creating the files, the CLI asks whether to auto-register the service in `src/services/services.config.ts`. If you accept, it adds the import and service entry. If you decline, it prints the snippet to add manually.
80
-
81
- ### `nile generate action <service-name> <action-name>`
54
+ The CLI then asks whether to register the service in `src/services/services.config.ts`. If you decline, it prints the lines to add yourself.
82
55
 
83
- Alias: `nile gen action <service-name> <action-name>`
84
-
85
- Generate a new action file in an existing service directory.
56
+ ### Actions
86
57
 
87
58
  ```bash
88
- nile gen action users get-user
59
+ bun run gen action users get-user
89
60
  ```
90
61
 
91
- Creates `src/services/users/get-user.ts` with:
62
+ Creates `src/services/users/actions/get-user.ts`:
92
63
 
93
- ```typescript
94
- import { type Action, createAction } from "@nilejs/nile";
95
- import { Ok } from "slang-ts";
64
+ ```ts
65
+ import { createAction } from "@nilejs/nile";
66
+ import { Ok } from "@nilejs/nile/fp";
96
67
  import z from "zod";
97
68
 
98
69
  const getUserSchema = z.object({
99
- // Define your validation schema here
70
+ // Describe the input this action accepts
100
71
  });
101
72
 
102
- const getUserHandler = async (data: Record<string, unknown>) => {
103
- // Implement your users.get-user logic here
104
- return Ok({ result: data });
73
+ type GetUserInput = z.infer<typeof getUserSchema>;
74
+
75
+ const getUserHandler = async (input: GetUserInput) => {
76
+ // Implement users.get-user here
77
+ return Ok({ result: input });
105
78
  };
106
79
 
107
- export const getUserAction: Action = createAction({
80
+ export const getUserAction = createAction<GetUserInput>({
108
81
  name: "get-user",
109
82
  description: "GetUser action for users",
110
- handler: getUserHandler,
111
83
  validation: getUserSchema,
84
+ handler: getUserHandler,
112
85
  });
113
86
  ```
114
87
 
115
- Kebab-case names are converted to camelCase for variables and PascalCase for types.
116
-
117
- ### `nile generate schema`
88
+ Kebab-case names become camelCase variables and PascalCase types. Register the action by importing it in `src/services/services.config.ts` and adding it to the service's `actions` list.
118
89
 
119
- Alias: `nile gen schema`
120
-
121
- Extract Zod validation schemas from your action definitions and generate TypeScript files with schema exports and inferred types.
90
+ ### Schemas and types
122
91
 
123
92
  ```bash
124
- nile gen schema
93
+ bun run gen schema
125
94
  ```
126
95
 
127
- The command auto-detects `src/services/services.config.ts`. If the file is not found, it prompts for the path. It spawns a `bun` subprocess to import your services config, extracts JSON Schema from each action's validation field, and converts them back to Zod code strings.
128
-
129
- Output (default `src/generated/`):
96
+ Reads every action's validation schema from `src/services/services.config.ts` and writes:
130
97
 
131
- - `schemas.ts` — named Zod schema exports (`tasksCreateSchema`, `tasksUpdateSchema`, etc.)
132
- - `types.ts` — TypeScript types via `z.infer` (`TasksCreatePayload`, `TasksUpdatePayload`, etc.)
133
-
134
- Options:
98
+ - `src/generated/schemas.ts`: named Zod schemas, such as `tasksCreateSchema`
99
+ - `src/generated/types.ts`: inferred types, such as `TasksCreatePayload`
135
100
 
136
101
  | Flag | Description |
137
102
  |---|---|
138
- | `-e, --entry <path>` | Path to services config (auto-detected by default) |
103
+ | `-e, --entry <path>` | Path to the services config (detected by default) |
139
104
  | `-o, --output <path>` | Output directory (default: `src/generated`) |
140
105
 
141
- Actions without a `validation` schema are skipped and listed in the CLI output.
142
-
143
- Requires [Bun](https://bun.sh) installed for the extraction subprocess.
144
-
145
- ### Quick Reference
146
-
147
- | Command | Description |
148
- |---|---|
149
- | `nile new <name>` | Scaffold a new project |
150
- | `nile gen service <name>` | Add a service with a demo action |
151
- | `nile gen action <service> <name>` | Add an action to an existing service |
152
- | `nile gen schema` | Generate Zod schemas and TypeScript types |
153
-
154
- ## Generated Project Structure
155
-
156
- ```
157
- my-app/
158
- package.json
159
- tsconfig.json
160
- drizzle.config.ts
161
- .env.example
162
- src/
163
- index.ts
164
- db/
165
- client.ts
166
- schema.ts
167
- types.ts
168
- index.ts
169
- models/
170
- tasks.ts
171
- index.ts
172
- services/
173
- services.config.ts
174
- tasks/
175
- create.ts
176
- list.ts
177
- get.ts
178
- update.ts
179
- delete.ts
180
- ```
106
+ Actions without a validation schema are skipped and listed in the output. Share the generated files with a frontend to validate forms with the same rules as the server.
181
107
 
182
108
  ## Requirements
183
109
 
184
- - Node.js 18+ or Bun
185
- - The scaffolded project uses Bun as its runtime (`Bun.serve`, `bun run`)
110
+ - [Bun](https://bun.sh) 1.4.2 or later. The starter runs on Bun, and `gen schema` imports your services with Bun.
186
111
 
187
112
  ## License
188
113
 
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  // src/index.ts
4
4
  import { resolve as resolve6 } from "node:path";
5
5
  import { Command } from "commander";
6
+ // package.json
7
+ var version = "1.0.0";
6
8
 
7
9
  // src/commands/generate-action.ts
8
10
  import { resolve } from "node:path";
@@ -87,47 +89,60 @@ var toPascalCase = (str) => {
87
89
  var generateActionContent = (actionName, serviceName) => {
88
90
  const camel = toCamelCase(actionName);
89
91
  const pascal = toPascalCase(actionName);
90
- return `import { type Action, createAction } from "@nilejs/nile";
91
- import { Ok } from "slang-ts";
92
+ return `import { createAction } from "@nilejs/nile";
93
+ import { Ok } from "@nilejs/nile/fp";
92
94
  import z from "zod";
93
95
 
94
96
  const ${camel}Schema = z.object({
95
- // Define your validation schema here
97
+ // Describe the input this action accepts
96
98
  });
97
99
 
98
- const ${camel}Handler = async (data: Record<string, unknown>) => {
99
- // Implement your ${serviceName}.${actionName} logic here
100
- return Ok({ result: data });
100
+ type ${pascal}Input = z.infer<typeof ${camel}Schema>;
101
+
102
+ const ${camel}Handler = async (input: ${pascal}Input) => {
103
+ // Implement ${serviceName}.${actionName} here
104
+ return Ok({ result: input });
101
105
  };
102
106
 
103
- export const ${camel}Action: Action = createAction({
107
+ export const ${camel}Action = createAction<${pascal}Input>({
104
108
  name: "${actionName}",
105
109
  description: "${pascal} action for ${serviceName}",
106
- handler: ${camel}Handler,
107
110
  validation: ${camel}Schema,
111
+ handler: ${camel}Handler,
108
112
  });
109
113
  `;
110
114
  };
111
- var generateActionCommand = async (serviceName, actionName) => {
115
+ var generateActionCommand = async (serviceName, actionName, options = {}) => {
112
116
  const serviceDir = resolve(process.cwd(), "src/services", serviceName);
113
117
  if (!pathExists(serviceDir)) {
114
118
  error(`Service "${serviceName}" not found at src/services/${serviceName}/`);
115
119
  hint(`Create the service first: nile generate service ${serviceName}`);
116
120
  process.exit(1);
117
121
  }
118
- const actionFile = resolve(serviceDir, `${actionName}.ts`);
119
- if (pathExists(actionFile)) {
122
+ const relative = options.folder ? `actions/${actionName}/${actionName}.ts` : `actions/${actionName}.ts`;
123
+ const actionFile = resolve(serviceDir, relative);
124
+ const existingEntries = [
125
+ actionFile,
126
+ resolve(serviceDir, `${actionName}.ts`),
127
+ resolve(serviceDir, `actions/${actionName}.ts`),
128
+ resolve(serviceDir, `actions/${actionName}/${actionName}.ts`)
129
+ ];
130
+ if (existingEntries.some(pathExists)) {
120
131
  error(`Action file "${actionName}.ts" already exists in src/services/${serviceName}/`);
121
132
  process.exit(1);
122
133
  }
123
134
  brand();
124
135
  const spinner = createSpinner(`Creating action ${actionName}...`);
125
136
  await writeFileSafe(actionFile, generateActionContent(actionName, serviceName));
126
- spinner.stop(`Action created at src/services/${serviceName}/${actionName}.ts`);
137
+ if (options.folder) {
138
+ await writeFileSafe(resolve(serviceDir, `actions/${actionName}/index.ts`), `export { ${toCamelCase(actionName)}Action } from "./${actionName}";
139
+ `);
140
+ }
141
+ spinner.stop(`Action created at src/services/${serviceName}/${relative}`);
127
142
  const camel = toCamelCase(actionName);
128
143
  console.log("");
129
144
  hint("Register the action in your service config:");
130
- hint(` import { ${camel}Action } from "./${serviceName}/${actionName}";`);
145
+ hint(` import { ${camel}Action } from "./${serviceName}/actions/${actionName}";`);
131
146
  outro();
132
147
  };
133
148
 
@@ -143,15 +158,15 @@ var confirmPrompt = (question, defaultYes = true) => {
143
158
  input: process.stdin,
144
159
  output: process.stdout
145
160
  });
146
- return new Promise((resolve2) => {
161
+ return new Promise((resolve) => {
147
162
  rl.question(` ${question} ${suffix} `, (answer) => {
148
163
  rl.close();
149
164
  const trimmed = answer.trim().toLowerCase();
150
165
  if (trimmed === "") {
151
- resolve2(defaultYes);
166
+ resolve(defaultYes);
152
167
  return;
153
168
  }
154
- resolve2(trimmed === "y" || trimmed === "yes");
169
+ resolve(trimmed === "y" || trimmed === "yes");
155
170
  });
156
171
  });
157
172
  };
@@ -160,10 +175,10 @@ var inputPrompt = (question) => {
160
175
  input: process.stdin,
161
176
  output: process.stdout
162
177
  });
163
- return new Promise((resolve2) => {
178
+ return new Promise((resolve) => {
164
179
  rl.question(` ${question} `, (answer) => {
165
180
  rl.close();
166
- resolve2(answer.trim());
181
+ resolve(answer.trim());
167
182
  });
168
183
  });
169
184
  };
@@ -313,7 +328,7 @@ var extractSchemas = (configPath, projectRoot) => {
313
328
  const importPath = resolveImportPath(configPath, projectRoot);
314
329
  const scriptContent = buildExtractionScript(importPath);
315
330
  const tempScriptPath = resolve2(projectRoot, TEMP_SCRIPT_NAME);
316
- return new Promise((resolve3, reject) => {
331
+ return new Promise((resolve, reject) => {
317
332
  writeFile2(tempScriptPath, scriptContent, "utf-8").then(() => {
318
333
  execFile("bun", ["run", TEMP_SCRIPT_NAME], { cwd: projectRoot, timeout: 30000 }, (err, stdout, stderr) => {
319
334
  unlink(tempScriptPath).catch(() => {});
@@ -325,7 +340,7 @@ ${message}`));
325
340
  }
326
341
  try {
327
342
  const result = JSON.parse(stdout.trim());
328
- resolve3(result);
343
+ resolve(result);
329
344
  } catch {
330
345
  reject(new Error(`Failed to parse extraction output. Raw output:
331
346
  ${stdout.trim()}`));
@@ -347,12 +362,12 @@ var DEFAULT_OUTPUT_DIR = "src/generated";
347
362
  var LEADING_SLASH2 = /^\//;
348
363
  var resolveConfigPath = async (projectRoot, entryFlag) => {
349
364
  if (entryFlag) {
350
- const absolute2 = resolve3(projectRoot, entryFlag);
351
- if (!pathExists(absolute2)) {
365
+ const absolute = resolve3(projectRoot, entryFlag);
366
+ if (!pathExists(absolute)) {
352
367
  error(`Config file not found: ${entryFlag}`);
353
368
  return null;
354
369
  }
355
- return absolute2;
370
+ return absolute;
356
371
  }
357
372
  const detected = findServicesConfig(projectRoot);
358
373
  if (detected) {
@@ -439,34 +454,36 @@ var toPascalCase2 = (str) => {
439
454
  var generateActionContent2 = (serviceName) => {
440
455
  const camel = toCamelCase3(serviceName);
441
456
  const pascal = toPascalCase2(serviceName);
442
- return `import { type Action, createAction } from "@nilejs/nile";
443
- import { Ok } from "slang-ts";
457
+ return `import { createAction } from "@nilejs/nile";
458
+ import { Ok } from "@nilejs/nile/fp";
444
459
  import z from "zod";
445
460
 
446
461
  const sample${pascal}Schema = z.object({
447
462
  name: z.string().min(1, "Name is required"),
448
463
  });
449
464
 
450
- const sample${pascal}Handler = async (data: Record<string, unknown>) => {
451
- return Ok({ ${camel}: { name: data.name } });
465
+ type Sample${pascal}Input = z.infer<typeof sample${pascal}Schema>;
466
+
467
+ const sample${pascal}Handler = async (input: Sample${pascal}Input) => {
468
+ return Ok({ ${camel}: { name: input.name } });
452
469
  };
453
470
 
454
- export const sample${pascal}Action: Action = createAction({
471
+ export const sample${pascal}Action = createAction<Sample${pascal}Input>({
455
472
  name: "sample",
456
473
  description: "Sample ${serviceName} action",
457
- handler: sample${pascal}Handler,
458
474
  validation: sample${pascal}Schema,
475
+ handler: sample${pascal}Handler,
459
476
  });
460
477
  `;
461
478
  };
462
479
  var generateBarrelContent = (serviceName) => {
463
480
  const pascal = toPascalCase2(serviceName);
464
- return `export { sample${pascal}Action } from "./sample";
481
+ return `export { sample${pascal}Action } from "./actions/sample";
465
482
  `;
466
483
  };
467
484
  var generateConfigSnippet = (serviceName) => {
468
485
  const pascal = toPascalCase2(serviceName);
469
- const importLine = `import { sample${pascal}Action } from "./${serviceName}/sample";`;
486
+ const importLine = `import { sample${pascal}Action } from "./${serviceName}/actions/sample";`;
470
487
  const serviceEntry = ` {
471
488
  name: "${serviceName}",
472
489
  description: "${pascal} service",
@@ -481,7 +498,7 @@ var autoRegisterService = async (configPath, serviceName) => {
481
498
  try {
482
499
  let content = await readFileContent(configPath);
483
500
  const pascal = toPascalCase2(serviceName);
484
- const importLine = `import { sample${pascal}Action } from "./${serviceName}/sample";`;
501
+ const importLine = `import { sample${pascal}Action } from "./${serviceName}/actions/sample";`;
485
502
  const importLines = content.split(`
486
503
  `).filter((line) => line.startsWith("import "));
487
504
  if (importLines.length === 0) {
@@ -498,7 +515,9 @@ ${importLine}`);
498
515
  description: "${pascal} service",
499
516
  actions: [sample${pascal}Action],
500
517
  },`;
501
- const closingIndex = content.lastIndexOf("];");
518
+ const wrappedClosingIndex = content.lastIndexOf("]);");
519
+ const plainClosingIndex = content.lastIndexOf("];");
520
+ const closingIndex = Math.max(wrappedClosingIndex, plainClosingIndex);
502
521
  if (closingIndex === -1) {
503
522
  return false;
504
523
  }
@@ -525,7 +544,7 @@ var generateServiceCommand = async (serviceName) => {
525
544
  brand();
526
545
  const spinner = createSpinner(`Creating service ${serviceName}...`);
527
546
  await ensureDir(serviceDir);
528
- await writeFileSafe(resolve4(serviceDir, "sample.ts"), generateActionContent2(serviceName));
547
+ await writeFileSafe(resolve4(serviceDir, "actions/sample.ts"), generateActionContent2(serviceName));
529
548
  await writeFileSafe(resolve4(serviceDir, "index.ts"), generateBarrelContent(serviceName));
530
549
  spinner.stop(`Service created at src/services/${serviceName}/`);
531
550
  const configPath = resolve4(servicesDir, "services.config.ts");
@@ -557,6 +576,7 @@ ${generateConfigSnippet(serviceName)}
557
576
  };
558
577
 
559
578
  // src/commands/new.ts
579
+ import { rename } from "node:fs/promises";
560
580
  import { resolve as resolve5 } from "node:path";
561
581
  import { fileURLToPath } from "node:url";
562
582
  var __dirname2 = fileURLToPath(new URL(".", import.meta.url));
@@ -581,6 +601,10 @@ var newCommand = async (projectName) => {
581
601
  const spinner = createSpinner(`Creating ${projectName}...`);
582
602
  const templateDir = resolveTemplateDir();
583
603
  await copyDir(templateDir, targetDir);
604
+ const shippedGitignore = resolve5(targetDir, "gitignore");
605
+ if (pathExists(shippedGitignore)) {
606
+ await rename(shippedGitignore, resolve5(targetDir, ".gitignore"));
607
+ }
584
608
  const allFiles = await getFilesRecursive(targetDir);
585
609
  const replacements = { "{{projectName}}": projectName };
586
610
  for (const filePath of allFiles) {
@@ -597,9 +621,9 @@ var newCommand = async (projectName) => {
597
621
  hint("bun run dev");
598
622
  console.log("");
599
623
  success("Generate with the CLI:");
600
- hint("nile g service <name> Add a new service");
601
- hint("nile g action <service> <name> Add an action to a service");
602
- hint("nile g schema Generate Zod schemas & types");
624
+ hint("bun run gen service <name> Add a new service");
625
+ hint("bun run gen action <service> <name> Add an action to a service");
626
+ hint("bun run gen schema Generate Zod schemas and types");
603
627
  outro();
604
628
  };
605
629
 
@@ -613,10 +637,10 @@ var ensureDepsInstalled = () => {
613
637
  }
614
638
  };
615
639
  var program = new Command;
616
- program.name("nile").description("CLI for the Nile backend framework").version("0.0.1");
640
+ program.name("nile").description("CLI for the Nile backend framework").version(version);
617
641
  program.command("new").argument("<project-name>", "Name of the project to create").description("Scaffold a new Nile project").action(newCommand);
618
642
  var generate = program.command("generate").alias("gen").description("Generate services and actions").hook("preAction", ensureDepsInstalled);
619
643
  generate.command("service").argument("<name>", "Service name (kebab-case recommended)").description("Generate a new service with a demo action").action(generateServiceCommand);
620
- generate.command("action").argument("<service-name>", "Name of the existing service").argument("<action-name>", "Name of the action to create").description("Generate a new action in an existing service").action(generateActionCommand);
644
+ generate.command("action").argument("<service-name>", "Name of the existing service").argument("<action-name>", "Name of the action to create").description("Generate a new action in an existing service").option("--folder", "Create an action folder for private modules").action(generateActionCommand);
621
645
  generate.command("schema").option("-e, --entry <path>", "Path to services config file").option("-o, --output <path>", "Output directory for generated files").description("Generate Zod schemas and TypeScript types from action validations").action(generateSchemaCommand);
622
646
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nilejs/cli",
3
- "version": "0.0.6",
3
+ "version": "1.0.0",
4
4
  "description": "CLI for scaffolding and generating Nile backend projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,10 +10,6 @@
10
10
  "dist",
11
11
  "template"
12
12
  ],
13
- "scripts": {
14
- "build": "bun run scripts/build.ts",
15
- "dev": "bun run src/index.ts"
16
- },
17
13
  "dependencies": {
18
14
  "commander": "^13.1.0",
19
15
  "json-schema-to-zod": "^2.7.0",
@@ -22,5 +18,11 @@
22
18
  "devDependencies": {
23
19
  "@types/node": "^22.8.1",
24
20
  "typescript": "^5"
21
+ },
22
+ "scripts": {
23
+ "build": "bun run scripts/build.ts",
24
+ "dev": "bun run src/index.ts",
25
+ "typecheck": "tsc --noEmit",
26
+ "export": "bun run typecheck && bun run build && node dist/index.js --version"
25
27
  }
26
- }
28
+ }
@@ -0,0 +1,12 @@
1
+ node_modules/
2
+ .git/
3
+ dist/
4
+ .env
5
+ .env.*
6
+ !.env.example
7
+ *.log
8
+ .DS_Store
9
+ *.local
10
+ coverage/
11
+ .vscode/
12
+ .idea/
@@ -0,0 +1,18 @@
1
+ FROM oven/bun:1-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package files
6
+ COPY package.json bun.lock ./
7
+
8
+ # Install production dependencies only
9
+ RUN bun install --frozen-lockfile --production
10
+
11
+ # Copy source code
12
+ COPY . .
13
+
14
+ # Expose the port (default for Nile)
15
+ EXPOSE 8000
16
+
17
+ # Start the server
18
+ CMD ["bun", "run", "src/index.ts"]
@@ -1,6 +1,8 @@
1
1
  # {{projectName}}
2
2
 
3
- Built with [Nile](https://github.com/nile-js/nile).
3
+ Built with [Nile Js](https://nile-js.github.io/nile/). Read the [getting started guide](https://nile-js.github.io/nile/guide/start/getting-started/) for a walkthrough of this starter.
4
+
5
+ The server runs on Bun's native HTTP server (`runtime: "bun", native: true` in `src/index.ts`). REST settings live under `rest`; custom routes and middleware are available on `server.nativeRest`.
4
6
 
5
7
  ## Setup
6
8
 
@@ -10,7 +12,7 @@ cp .env.example .env
10
12
  bun run dev
11
13
  ```
12
14
 
13
- The server starts at `http://localhost:8000`. PGLite creates an embedded Postgres database automatically, no external database required.
15
+ The server starts at `http://localhost:8000`. PGlite creates an embedded Postgres database in `data/` on first start, so no database server is required. The first start can take up to 30 seconds.
14
16
 
15
17
  ## Scripts
16
18
 
@@ -24,13 +26,13 @@ The server starts at `http://localhost:8000`. PGLite creates an embedded Postgre
24
26
  | `bun run db:push` | Push schema changes directly to the database |
25
27
  | `bun run db:studio` | Open Drizzle Studio to browse your data |
26
28
 
27
- ## Project Structure
29
+ ## Project structure
28
30
 
29
31
  ```
30
32
  src/
31
33
  index.ts # Server entry point
32
34
  db/
33
- client.ts # PGLite + Drizzle client
35
+ client.ts # PGlite + Drizzle client
34
36
  schema.ts # Drizzle table definitions
35
37
  types.ts # Inferred types from schema
36
38
  index.ts # Barrel export
@@ -39,7 +41,7 @@ src/
39
41
  index.ts # Barrel export
40
42
  services/
41
43
  services.config.ts # Service registry
42
- tasks/
44
+ tasks/actions/
43
45
  create.ts # Create task action
44
46
  list.ts # List tasks action
45
47
  get.ts # Get task by ID action
@@ -75,7 +77,7 @@ curl -X POST http://localhost:8000/api/services \
75
77
  -d '{"intent":"schema","service":"tasks","action":"*","payload":{}}'
76
78
  ```
77
79
 
78
- ## Adding Services
80
+ ## Adding services
79
81
 
80
82
  Generate a new service with the CLI:
81
83
 
@@ -83,9 +85,9 @@ Generate a new service with the CLI:
83
85
  bun run gen service users
84
86
  ```
85
87
 
86
- Or manually create a directory under `src/services/` with action files and register it in `src/services/services.config.ts`.
88
+ The generator offers to register the service in `src/services/services.config.ts`. To add one by hand, create a folder under `src/services/` with an `actions/` folder and register its actions in `services.config.ts`.
87
89
 
88
- ## Adding Actions
90
+ ## Adding actions
89
91
 
90
92
  Generate a new action in an existing service:
91
93
 
@@ -93,9 +95,9 @@ Generate a new action in an existing service:
93
95
  bun run gen action users get-user
94
96
  ```
95
97
 
96
- Each action file exports a single action created with `createAction`, which takes a Zod validation schema and a handler function that returns `Ok(data)` or `Err(message)`.
98
+ This creates `src/services/users/actions/get-user.ts`. Each action file exports one action created with `createAction`: a named Zod schema, an input type inferred from it and a named handler that returns `Ok(data)` or `Err(message)`. Import the action in `src/services/services.config.ts` and add it to the service's `actions` list.
97
99
 
98
- ## Generating Schemas & Types
100
+ ## Generating schemas and types
99
101
 
100
102
  Extract Zod validation schemas from your actions and generate TypeScript types:
101
103
 
@@ -105,8 +107,8 @@ bun run gen schema
105
107
 
106
108
  This auto-detects `src/services/services.config.ts`, reads validation schemas from all actions, and outputs two files:
107
109
 
108
- - `src/generated/schemas.ts` — named Zod schema exports
109
- - `src/generated/types.ts` — inferred TypeScript types via `z.infer`
110
+ - `src/generated/schemas.ts`: named Zod schema exports
111
+ - `src/generated/types.ts`: inferred TypeScript types via `z.infer`
110
112
 
111
113
  Options:
112
114
 
@@ -119,14 +121,14 @@ Actions without a validation schema are skipped and listed in the CLI output.
119
121
 
120
122
  ## Database
121
123
 
122
- This project ships with [PGLite](https://electric-sql.com/product/pglite) for zero-setup local development. For production, you can swap it for Postgres, MySQL, SQLite, or any other database supported by Drizzle. Update `src/db/client.ts` with your connection and driver.
124
+ This project ships with [PGlite](https://electric-sql.com/product/pglite) for zero-setup local development. For production, you can swap it for Postgres, MySQL, SQLite, or any other database supported by Drizzle. Update `src/db/client.ts` with your connection and driver.
123
125
 
124
126
  See the [Drizzle getting started guide](https://orm.drizzle.team/docs/get-started) for setup instructions with each supported database.
125
127
 
126
- ## Tech Stack
128
+ ## Tech stack
127
129
 
128
130
  - **Runtime:** [Bun](https://bun.sh)
129
- - **Framework:** [@nilejs/nile](https://github.com/nile-js/nile/tree/main/packages/nile)
130
- - **Database:** [PGLite](https://electric-sql.com/product/pglite) (embedded Postgres)
131
+ - **Framework:** [Nile Js](https://nile-js.github.io/nile/) (`@nilejs/nile`)
132
+ - **Database:** [PGlite](https://electric-sql.com/product/pglite) (embedded Postgres)
131
133
  - **ORM:** [Drizzle](https://orm.drizzle.team)
132
134
  - **Validation:** [Zod](https://zod.dev)
@@ -0,0 +1,13 @@
1
+
2
+ services:
3
+ nile:
4
+ build:
5
+ context: .
6
+ dockerfile: Dockerfile
7
+ ports:
8
+ - "8000:8000"
9
+ environment:
10
+ - NODE_ENV=development
11
+ volumes:
12
+ - ./data:/app/data
13
+ restart: unless-stopped
@@ -0,0 +1,16 @@
1
+ services:
2
+ nile:
3
+ build:
4
+ context: .
5
+ dockerfile: Dockerfile
6
+ ports:
7
+ - "8000:8000"
8
+ environment:
9
+ - NODE_ENV=production
10
+ volumes:
11
+ # The starter keeps its PGlite database in ./data; this volume keeps it across restarts.
12
+ - nile_data:/app/data
13
+ restart: unless-stopped
14
+
15
+ volumes:
16
+ nile_data:
@@ -0,0 +1,16 @@
1
+ # Dependencies
2
+ node_modules/
3
+
4
+ # Local environment
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+
9
+ # Local PGlite database and logs
10
+ data/
11
+ logs/
12
+ *.log
13
+
14
+ # Build output and generated files
15
+ dist/
16
+ .DS_Store
@@ -4,7 +4,7 @@
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "scripts": {
7
- "dev": "bun run src/index.ts",
7
+ "dev": "bun --watch src/index.ts",
8
8
  "gen": "bunx @nilejs/cli generate",
9
9
  "db:generate": "bunx drizzle-kit generate",
10
10
  "db:push": "bunx drizzle-kit push",
@@ -15,7 +15,6 @@
15
15
  "@electric-sql/pglite": "^0.2.5",
16
16
  "drizzle-orm": "^0.45.2",
17
17
  "pino": "^10.3.1",
18
- "slang-ts": "^0.0.7",
19
18
  "zod": "^4.3.6"
20
19
  },
21
20
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  import { createLogger, createNileServer } from "@nilejs/nile";
2
2
  import { sql } from "drizzle-orm";
3
- import { Err, Ok, safeTry } from "slang-ts";
3
+ import { Ok, safeTry } from "@nilejs/nile/fp";
4
4
  import { db } from "@/db/client";
5
5
  import { services } from "@/services/services.config";
6
6
 
@@ -31,6 +31,9 @@ const pushSchema = async () => {
31
31
 
32
32
  const server = await createNileServer({
33
33
  serverName: "{{projectName}}",
34
+ runtime: "bun",
35
+ native: true,
36
+ project: { structure: "error" },
34
37
  services,
35
38
  resources: { logger, database: db },
36
39
  rest: {
@@ -42,6 +45,8 @@ const server = await createNileServer({
42
45
  discovery: { enabled: true },
43
46
  },
44
47
  onBoot: {
48
+ // The first PGlite start on a slow machine can take longer than the 10 second default.
49
+ maxWaitTime: 30_000,
45
50
  fn: async (ctx) => {
46
51
  await pushSchema();
47
52
  ctx.resources?.logger?.info({
@@ -54,18 +59,19 @@ const server = await createNileServer({
54
59
  });
55
60
 
56
61
  // Register custom middleware (runs before all service requests)
57
- server.rest?.addMiddleware("/api", async (c, next) => {
62
+ server.nativeRest?.addMiddleware("/api", async (c, next) => {
58
63
  const start = performance.now();
59
64
  await next();
60
65
  const ms = (performance.now() - start).toFixed(1);
61
66
  logger.info({
62
67
  atFunction: "requestLog",
63
- message: `${c.req.method} ${c.req.path} - ${ms}ms`,
68
+ message: `${c.request.method} ${c.path} - ${ms}ms`,
64
69
  });
70
+ return undefined;
65
71
  });
66
72
 
67
- if (server.rest) {
73
+ if (server.nativeRest) {
68
74
  const port = server.config.rest?.port ?? 8000;
69
- Bun.serve({ port, fetch: server.rest.app.fetch });
75
+ Bun.serve({ port, fetch: server.nativeRest.fetch });
70
76
  console.log(`\n{{projectName}} listening on http://localhost:${port}\n`);
71
77
  }
@@ -1,9 +1,9 @@
1
1
  import { createServices, type Services } from "@nilejs/nile";
2
- import { createTaskAction } from "./tasks/create";
3
- import { deleteTaskAction } from "./tasks/delete";
4
- import { getTaskAction } from "./tasks/get";
5
- import { listTaskAction } from "./tasks/list";
6
- import { updateTaskAction } from "./tasks/update";
2
+ import { createTaskAction } from "./tasks/actions/create";
3
+ import { deleteTaskAction } from "./tasks/actions/delete";
4
+ import { getTaskAction } from "./tasks/actions/get";
5
+ import { listTaskAction } from "./tasks/actions/list";
6
+ import { updateTaskAction } from "./tasks/actions/update";
7
7
 
8
8
  export const services: Services = createServices([
9
9
  {
@@ -1,5 +1,5 @@
1
- import { type Action, createAction } from "@nilejs/nile";
2
- import { Err, Ok } from "slang-ts";
1
+ import { createAction } from "@nilejs/nile";
2
+ import { Err, Ok } from "@nilejs/nile/fp";
3
3
  import z from "zod";
4
4
  import { taskModel } from "@/db/models";
5
5
 
@@ -12,23 +12,27 @@ const createTaskSchema = z.object({
12
12
  .default("pending"),
13
13
  });
14
14
 
15
- const createTaskHandler = async (data: Record<string, unknown>) => {
15
+ type CreateTaskInput = z.infer<typeof createTaskSchema>;
16
+
17
+ const createTaskHandler = async (input: CreateTaskInput) => {
16
18
  const result = await taskModel.create({
17
19
  data: {
18
- title: data.title as string,
19
- description: (data.description as string) ?? "",
20
- status: (data.status as "pending" | "in-progress" | "done") ?? "pending",
20
+ title: input.title,
21
+ description: input.description,
22
+ status: input.status,
21
23
  },
22
24
  });
25
+
23
26
  if (result.isErr) {
24
27
  return Err(result.error);
25
28
  }
29
+
26
30
  return Ok({ task: result.value });
27
31
  };
28
32
 
29
- export const createTaskAction: Action = createAction({
33
+ export const createTaskAction = createAction<CreateTaskInput>({
30
34
  name: "create",
31
35
  description: "Create a new task",
32
- handler: createTaskHandler,
33
36
  validation: createTaskSchema,
37
+ handler: createTaskHandler,
34
38
  });
@@ -0,0 +1,27 @@
1
+ import { createAction } from "@nilejs/nile";
2
+ import { Err, Ok } from "@nilejs/nile/fp";
3
+ import z from "zod";
4
+ import { taskModel } from "@/db/models";
5
+
6
+ const deleteTaskSchema = z.object({
7
+ id: z.string().min(1, "Task ID is required"),
8
+ });
9
+
10
+ type DeleteTaskInput = z.infer<typeof deleteTaskSchema>;
11
+
12
+ const deleteTaskHandler = async (input: DeleteTaskInput) => {
13
+ const result = await taskModel.delete(input.id);
14
+
15
+ if (result.isErr) {
16
+ return Err(result.error);
17
+ }
18
+
19
+ return Ok({ deleted: true, id: input.id });
20
+ };
21
+
22
+ export const deleteTaskAction = createAction<DeleteTaskInput>({
23
+ name: "delete",
24
+ description: "Delete a task by ID",
25
+ validation: deleteTaskSchema,
26
+ handler: deleteTaskHandler,
27
+ });
@@ -1,5 +1,5 @@
1
- import { type Action, createAction } from "@nilejs/nile";
2
- import { Err, Ok } from "slang-ts";
1
+ import { createAction } from "@nilejs/nile";
2
+ import { Err, Ok } from "@nilejs/nile/fp";
3
3
  import z from "zod";
4
4
  import { taskModel } from "@/db/models";
5
5
 
@@ -7,17 +7,21 @@ const getTaskSchema = z.object({
7
7
  id: z.string().min(1, "Task ID is required"),
8
8
  });
9
9
 
10
- const getTaskHandler = async (data: Record<string, unknown>) => {
11
- const result = await taskModel.findById(data.id as string);
10
+ type GetTaskInput = z.infer<typeof getTaskSchema>;
11
+
12
+ const getTaskHandler = async (input: GetTaskInput) => {
13
+ const result = await taskModel.findById(input.id);
14
+
12
15
  if (result.isErr) {
13
16
  return Err(result.error);
14
17
  }
18
+
15
19
  return Ok({ task: result.value });
16
20
  };
17
21
 
18
- export const getTaskAction: Action = createAction({
22
+ export const getTaskAction = createAction<GetTaskInput>({
19
23
  name: "get",
20
24
  description: "Get a task by ID",
21
- handler: getTaskHandler,
22
25
  validation: getTaskSchema,
26
+ handler: getTaskHandler,
23
27
  });
@@ -1,16 +1,18 @@
1
- import { type Action, createAction } from "@nilejs/nile";
2
- import { Err, Ok } from "slang-ts";
1
+ import { createAction } from "@nilejs/nile";
2
+ import { Err, Ok } from "@nilejs/nile/fp";
3
3
  import { taskModel } from "@/db/models";
4
4
 
5
5
  const listTasksHandler = async () => {
6
6
  const result = await taskModel.findAll();
7
+
7
8
  if (result.isErr) {
8
9
  return Err(result.error);
9
10
  }
11
+
10
12
  return Ok({ tasks: result.value });
11
13
  };
12
14
 
13
- export const listTaskAction: Action = createAction({
15
+ export const listTaskAction = createAction({
14
16
  name: "list",
15
17
  description: "List all tasks",
16
18
  handler: listTasksHandler,
@@ -1,5 +1,5 @@
1
- import { type Action, createAction } from "@nilejs/nile";
2
- import { Err, Ok } from "slang-ts";
1
+ import { createAction } from "@nilejs/nile";
2
+ import { Err, Ok } from "@nilejs/nile/fp";
3
3
  import z from "zod";
4
4
  import { taskModel } from "@/db/models";
5
5
 
@@ -10,22 +10,29 @@ const updateTaskSchema = z.object({
10
10
  status: z.enum(["pending", "in-progress", "done"]).optional(),
11
11
  });
12
12
 
13
- const updateTaskHandler = async (data: Record<string, unknown>) => {
14
- const { id, ...updates } = data;
13
+ type UpdateTaskInput = z.infer<typeof updateTaskSchema>;
15
14
 
15
+ const updateTaskHandler = async (input: UpdateTaskInput) => {
16
+ // Fields left out of the request stay undefined and are not changed.
16
17
  const result = await taskModel.update({
17
- id: id as string,
18
- data: updates,
18
+ id: input.id,
19
+ data: {
20
+ title: input.title,
21
+ description: input.description,
22
+ status: input.status,
23
+ },
19
24
  });
25
+
20
26
  if (result.isErr) {
21
27
  return Err(result.error);
22
28
  }
29
+
23
30
  return Ok({ task: result.value });
24
31
  };
25
32
 
26
- export const updateTaskAction: Action = createAction({
33
+ export const updateTaskAction = createAction<UpdateTaskInput>({
27
34
  name: "update",
28
35
  description: "Update an existing task",
29
- handler: updateTaskHandler,
30
36
  validation: updateTaskSchema,
37
+ handler: updateTaskHandler,
31
38
  });
@@ -1,23 +0,0 @@
1
- import { type Action, createAction } from "@nilejs/nile";
2
- import { Err, Ok } from "slang-ts";
3
- import z from "zod";
4
- import { taskModel } from "@/db/models";
5
-
6
- const deleteTaskSchema = z.object({
7
- id: z.string().min(1, "Task ID is required"),
8
- });
9
-
10
- const deleteTaskHandler = async (data: Record<string, unknown>) => {
11
- const result = await taskModel.delete(data.id as string);
12
- if (result.isErr) {
13
- return Err(result.error);
14
- }
15
- return Ok({ deleted: true, id: data.id });
16
- };
17
-
18
- export const deleteTaskAction: Action = createAction({
19
- name: "delete",
20
- description: "Delete a task by ID",
21
- handler: deleteTaskHandler,
22
- validation: deleteTaskSchema,
23
- });