create-softeneers-app 0.1.0 → 0.2.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/README.html +56 -0
  2. package/README.md +16 -8
  3. package/dist/args.js +33 -4
  4. package/dist/args.js.map +1 -1
  5. package/dist/fragments.js +127 -0
  6. package/dist/fragments.js.map +1 -0
  7. package/dist/index.js +18 -7
  8. package/dist/index.js.map +1 -1
  9. package/dist/prompts.js +27 -1
  10. package/dist/prompts.js.map +1 -1
  11. package/dist/templates.js +33 -8
  12. package/dist/templates.js.map +1 -1
  13. package/package.json +2 -2
  14. package/templates/express-api/.env.example +13 -0
  15. package/templates/express-api/README.md +77 -0
  16. package/templates/express-api/docker-compose.yml +15 -0
  17. package/templates/express-api/package.json +36 -0
  18. package/templates/express-api/softeneers.template.json +17 -0
  19. package/templates/express-api/src/auth/auth.ts +11 -0
  20. package/templates/express-api/src/cars/routes.ts +58 -0
  21. package/templates/express-api/src/cars/store.ts +92 -0
  22. package/templates/express-api/src/cars/types.ts +8 -0
  23. package/templates/express-api/src/cars/validate.ts +16 -0
  24. package/templates/express-api/src/db.ts +13 -0
  25. package/templates/express-api/src/env.ts +23 -0
  26. package/templates/express-api/src/index.ts +55 -0
  27. package/templates/express-api/src/scripts/migrate.ts +13 -0
  28. package/templates/express-api/src/scripts/seed.ts +25 -0
  29. package/templates/express-api/test/validate.test.ts +25 -0
  30. package/templates/express-api/tsconfig.json +14 -0
  31. package/templates/hono-api/.env.example +13 -0
  32. package/templates/hono-api/README.md +77 -0
  33. package/templates/hono-api/docker-compose.yml +15 -0
  34. package/templates/hono-api/package.json +34 -0
  35. package/templates/hono-api/softeneers.template.json +17 -0
  36. package/templates/hono-api/src/auth/auth.ts +11 -0
  37. package/templates/hono-api/src/cars/routes.ts +43 -0
  38. package/templates/hono-api/src/cars/store.ts +92 -0
  39. package/templates/hono-api/src/cars/types.ts +8 -0
  40. package/templates/hono-api/src/cars/validate.ts +16 -0
  41. package/templates/hono-api/src/db.ts +13 -0
  42. package/templates/hono-api/src/env.ts +23 -0
  43. package/templates/hono-api/src/index.ts +44 -0
  44. package/templates/hono-api/src/scripts/migrate.ts +13 -0
  45. package/templates/hono-api/src/scripts/seed.ts +25 -0
  46. package/templates/hono-api/test/validate.test.ts +25 -0
  47. package/templates/hono-api/tsconfig.json +14 -0
  48. package/templates/minimal/.env.example +2 -0
  49. package/templates/minimal/README.md +33 -0
  50. package/templates/minimal/package.json +22 -0
  51. package/templates/minimal/src/index.ts +20 -0
  52. package/templates/minimal/test/greet.test.ts +12 -0
  53. package/templates/minimal/tsconfig.json +15 -0
  54. package/templates/tanstack-start/.env.example +11 -0
  55. package/templates/tanstack-start/README.md +73 -0
  56. package/templates/tanstack-start/docker-compose.yml +15 -0
  57. package/templates/tanstack-start/package.json +56 -0
  58. package/templates/tanstack-start/public/favicon.ico +0 -0
  59. package/templates/tanstack-start/public/logo192.png +0 -0
  60. package/templates/tanstack-start/public/logo512.png +0 -0
  61. package/templates/tanstack-start/public/manifest.json +25 -0
  62. package/templates/tanstack-start/public/robots.txt +3 -0
  63. package/templates/tanstack-start/softeneers.template.json +17 -0
  64. package/templates/tanstack-start/src/cars/types.ts +8 -0
  65. package/templates/tanstack-start/src/cars/validate.ts +16 -0
  66. package/templates/tanstack-start/src/router.tsx +19 -0
  67. package/templates/tanstack-start/src/routes/__root.tsx +54 -0
  68. package/templates/tanstack-start/src/routes/api/auth/$.ts +14 -0
  69. package/templates/tanstack-start/src/routes/cars.tsx +87 -0
  70. package/templates/tanstack-start/src/routes/index.tsx +20 -0
  71. package/templates/tanstack-start/src/server/auth.ts +11 -0
  72. package/templates/tanstack-start/src/server/cars.ts +27 -0
  73. package/templates/tanstack-start/src/server/db.ts +12 -0
  74. package/templates/tanstack-start/src/server/env.ts +22 -0
  75. package/templates/tanstack-start/src/server/scripts/migrate.ts +13 -0
  76. package/templates/tanstack-start/src/server/scripts/seed.ts +25 -0
  77. package/templates/tanstack-start/src/server/store.ts +79 -0
  78. package/templates/tanstack-start/src/styles.css +17 -0
  79. package/templates/tanstack-start/tsconfig.json +28 -0
  80. package/templates/tanstack-start/tsr.config.json +3 -0
  81. package/templates/tanstack-start/vite.config.ts +14 -0
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "express-api",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Express + TypeScript REST API generated by create-softeneers-app.",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "scripts": {
11
+ "dev": "tsx watch src/index.ts",
12
+ "build": "tsc",
13
+ "start": "node dist/index.js",
14
+ "typecheck": "tsc --noEmit",
15
+ "db:migrate": "tsx src/scripts/migrate.ts",
16
+ "db:seed": "tsx src/scripts/seed.ts",
17
+ "db:reset": "tsx src/scripts/migrate.ts --reset && tsx src/scripts/seed.ts",
18
+ "test": "node --import tsx --test test/*.test.ts"
19
+ },
20
+ "dependencies": {
21
+ "@softeneers/auth": "^0.1.0",
22
+ "@softeneers/db": "^0.1.0",
23
+ "@softeneers/env": "^0.1.0",
24
+ "cors": "^2.8.5",
25
+ "dotenv": "^16.4.5",
26
+ "express": "^5.0.0",
27
+ "mysql2": "^3.11.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/cors": "^2.8.17",
31
+ "@types/express": "^5.0.0",
32
+ "@types/node": "^20.0.0",
33
+ "tsx": "^4.19.0",
34
+ "typescript": "^5.6.0"
35
+ }
36
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "toggles": { "db": true, "auth": false, "docker": true },
3
+ "fragments": {
4
+ "db": {
5
+ "removePaths": ["src/db.ts", "src/scripts", "docker-compose.yml"],
6
+ "removeDeps": ["@softeneers/db", "mysql2"],
7
+ "removeScripts": ["db:migrate", "db:seed", "db:reset"]
8
+ },
9
+ "auth": {
10
+ "removePaths": ["src/auth"],
11
+ "removeDeps": ["@softeneers/auth"]
12
+ },
13
+ "docker": {
14
+ "removePaths": ["docker-compose.yml"]
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,11 @@
1
+ import { createAuth } from "@softeneers/auth";
2
+
3
+ import { env } from "../env.js";
4
+
5
+ // Email + password authentication via better-auth. With no `database` configured
6
+ // better-auth uses an in-memory store (fine for local dev); point it at your DB
7
+ // for persistence. Routes are mounted at /api/auth/* in src/index.ts.
8
+ export const auth = createAuth({
9
+ secret: env.AUTH_SECRET,
10
+ baseURL: env.AUTH_BASE_URL,
11
+ });
@@ -0,0 +1,58 @@
1
+ import { Router } from "express";
2
+
3
+ import { carStore } from "./store.js";
4
+ import { parseNewCar, ValidationError } from "./validate.js";
5
+
6
+ export const carRouter = Router();
7
+
8
+ carRouter.get("/", async (_req, res) => {
9
+ res.json(await carStore.list());
10
+ });
11
+
12
+ carRouter.get("/:id", async (req, res) => {
13
+ const car = await carStore.get(Number(req.params.id));
14
+ if (!car) {
15
+ res.status(404).json({ message: "Car not found." });
16
+ return;
17
+ }
18
+ res.json(car);
19
+ });
20
+
21
+ carRouter.post("/", async (req, res) => {
22
+ try {
23
+ const car = await carStore.create(parseNewCar(req.body));
24
+ res.status(201).json(car);
25
+ } catch (error) {
26
+ if (error instanceof ValidationError) {
27
+ res.status(400).json({ message: error.message });
28
+ return;
29
+ }
30
+ throw error;
31
+ }
32
+ });
33
+
34
+ carRouter.put("/:id", async (req, res) => {
35
+ try {
36
+ const car = await carStore.update(Number(req.params.id), parseNewCar(req.body));
37
+ if (!car) {
38
+ res.status(404).json({ message: "Car not found." });
39
+ return;
40
+ }
41
+ res.json(car);
42
+ } catch (error) {
43
+ if (error instanceof ValidationError) {
44
+ res.status(400).json({ message: error.message });
45
+ return;
46
+ }
47
+ throw error;
48
+ }
49
+ });
50
+
51
+ carRouter.delete("/:id", async (req, res) => {
52
+ const removed = await carStore.remove(Number(req.params.id));
53
+ if (!removed) {
54
+ res.status(404).json({ message: "Car not found." });
55
+ return;
56
+ }
57
+ res.status(204).end();
58
+ });
@@ -0,0 +1,92 @@
1
+ import type { Car, NewCar } from "./types.js";
2
+
3
+ // The data layer behind the CRUD routes. With the `db` toggle on this is backed
4
+ // by MySQL (Sequelize via @softeneers/db); with it off it's an in-memory Map so
5
+ // the API runs with zero infrastructure. Both satisfy the same CarStore.
6
+ export interface CarStore {
7
+ list(): Promise<Car[]>;
8
+ get(id: number): Promise<Car | null>;
9
+ create(input: NewCar): Promise<Car>;
10
+ update(id: number, input: NewCar): Promise<Car | null>;
11
+ remove(id: number): Promise<boolean>;
12
+ }
13
+
14
+ // #if db
15
+ import { DataTypes, Model } from "@softeneers/db";
16
+
17
+ import { sequelize } from "../db.js";
18
+
19
+ class CarModel extends Model {
20
+ declare id: number;
21
+ declare brand: string;
22
+ declare model: string;
23
+ declare year: number;
24
+ }
25
+
26
+ CarModel.init(
27
+ {
28
+ id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
29
+ brand: { type: DataTypes.STRING, allowNull: false },
30
+ model: { type: DataTypes.STRING, allowNull: false },
31
+ year: { type: DataTypes.INTEGER, allowNull: false },
32
+ },
33
+ { sequelize, modelName: "Car", tableName: "cars" },
34
+ );
35
+
36
+ export { CarModel };
37
+
38
+ const toCar = (m: CarModel): Car => ({ id: m.id, brand: m.brand, model: m.model, year: m.year });
39
+
40
+ export const carStore: CarStore = {
41
+ async list() {
42
+ return (await CarModel.findAll({ order: [["id", "ASC"]] })).map(toCar);
43
+ },
44
+ async get(id) {
45
+ const m = await CarModel.findByPk(id);
46
+ return m ? toCar(m) : null;
47
+ },
48
+ async create(input) {
49
+ return toCar(await CarModel.create(input));
50
+ },
51
+ async update(id, input) {
52
+ const m = await CarModel.findByPk(id);
53
+ if (!m) return null;
54
+ await m.update(input);
55
+ return toCar(m);
56
+ },
57
+ async remove(id) {
58
+ const m = await CarModel.findByPk(id);
59
+ if (!m) return false;
60
+ await m.destroy();
61
+ return true;
62
+ },
63
+ };
64
+ // #endif
65
+ // #if !db
66
+ let nextId = 1;
67
+ const cars = new Map<number, Car>();
68
+
69
+ export const carStore: CarStore = {
70
+ async list() {
71
+ return [...cars.values()].sort((a, b) => a.id - b.id);
72
+ },
73
+ async get(id) {
74
+ return cars.get(id) ?? null;
75
+ },
76
+ async create(input) {
77
+ const car: Car = { id: nextId++, ...input };
78
+ cars.set(car.id, car);
79
+ return car;
80
+ },
81
+ async update(id, input) {
82
+ const car = cars.get(id);
83
+ if (!car) return null;
84
+ const next: Car = { ...car, ...input };
85
+ cars.set(id, next);
86
+ return next;
87
+ },
88
+ async remove(id) {
89
+ return cars.delete(id);
90
+ },
91
+ };
92
+ // #endif
@@ -0,0 +1,8 @@
1
+ export interface Car {
2
+ id: number;
3
+ brand: string;
4
+ model: string;
5
+ year: number;
6
+ }
7
+
8
+ export type NewCar = Omit<Car, "id">;
@@ -0,0 +1,16 @@
1
+ import type { NewCar } from "./types.js";
2
+
3
+ /** Thrown when a request body is not a valid car. Mapped to HTTP 400. */
4
+ export class ValidationError extends Error {}
5
+
6
+ /** Validate and normalize an untrusted request body into a NewCar. */
7
+ export function parseNewCar(body: unknown): NewCar {
8
+ const b = (body ?? {}) as Record<string, unknown>;
9
+ const brand = typeof b.brand === "string" ? b.brand.trim() : "";
10
+ const model = typeof b.model === "string" ? b.model.trim() : "";
11
+ const year = Number(b.year);
12
+ if (!brand || !model || !Number.isInteger(year)) {
13
+ throw new ValidationError("brand (string), model (string) and year (integer) are required.");
14
+ }
15
+ return { brand, model, year };
16
+ }
@@ -0,0 +1,13 @@
1
+ import { createDb } from "@softeneers/db";
2
+
3
+ import { env } from "./env.js";
4
+
5
+ // A configured (but not-yet-connected) Sequelize instance. Importing a model
6
+ // registers it; `assertConnection`/`sync` open the connection on startup.
7
+ export const sequelize = createDb({
8
+ host: env.DB_HOST,
9
+ port: env.DB_PORT,
10
+ database: env.DB_NAME,
11
+ username: env.DB_USER,
12
+ password: env.DB_PASSWORD,
13
+ });
@@ -0,0 +1,23 @@
1
+ import "dotenv/config";
2
+
3
+ import { createEnv, z } from "@softeneers/env";
4
+
5
+ // Validated, typed environment. Boot fails fast with a readable message if the
6
+ // .env is wrong. Variables for a feature exist only when that toggle is on.
7
+ export const env = createEnv({
8
+ schema: {
9
+ PORT: z.coerce.number().default(4000),
10
+ CORS_ORIGIN: z.string().default("http://localhost:3000"),
11
+ // #if db
12
+ DB_HOST: z.string().default("127.0.0.1"),
13
+ DB_PORT: z.coerce.number().default(3306),
14
+ DB_NAME: z.string().default("app_dev"),
15
+ DB_USER: z.string().default("root"),
16
+ DB_PASSWORD: z.string().default(""),
17
+ // #endif
18
+ // #if auth
19
+ AUTH_SECRET: z.string().min(16).default("dev-secret-change-me-to-a-long-random-string"),
20
+ AUTH_BASE_URL: z.string().default("http://localhost:4000"),
21
+ // #endif
22
+ },
23
+ });
@@ -0,0 +1,55 @@
1
+ import cors from "cors";
2
+ import express from "express";
3
+
4
+ import { carRouter } from "./cars/routes.js";
5
+ import { env } from "./env.js";
6
+ // #if db
7
+ import { assertConnection } from "@softeneers/db";
8
+
9
+ import { sequelize } from "./db.js";
10
+ import "./cars/store.js"; // registers the Car model on the connection
11
+ // #endif
12
+ // #if auth
13
+ import { toNodeHandler } from "@softeneers/auth";
14
+
15
+ import { auth } from "./auth/auth.js";
16
+ // #endif
17
+
18
+ const app = express();
19
+
20
+ // #if auth
21
+ // better-auth handles its own body parsing, so mount it before express.json().
22
+ app.all("/api/auth/*splat", toNodeHandler(auth));
23
+ // #endif
24
+
25
+ app.use(cors({ origin: env.CORS_ORIGIN }));
26
+ app.use(express.json());
27
+
28
+ app.get("/health", (_req, res) => {
29
+ res.json({ ok: true });
30
+ });
31
+
32
+ app.use("/api/cars", carRouter);
33
+
34
+ app.use(
35
+ (error: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
36
+ console.error("Unhandled error:", error);
37
+ res.status(500).json({ message: "Internal server error." });
38
+ },
39
+ );
40
+
41
+ async function start(): Promise<void> {
42
+ // #if db
43
+ await assertConnection(sequelize);
44
+ await sequelize.sync();
45
+ console.log("Database connected.");
46
+ // #endif
47
+ app.listen(env.PORT, () => {
48
+ console.log(`API running on http://localhost:${env.PORT}`);
49
+ });
50
+ }
51
+
52
+ start().catch((error) => {
53
+ console.error("Failed to start:", error);
54
+ process.exit(1);
55
+ });
@@ -0,0 +1,13 @@
1
+ import "dotenv/config";
2
+
3
+ import { assertConnection } from "@softeneers/db";
4
+
5
+ import { sequelize } from "../db.js";
6
+ import "../cars/store.js"; // importing the store registers the Car model
7
+
8
+ const reset = process.argv.includes("--reset");
9
+
10
+ await assertConnection(sequelize);
11
+ await sequelize.sync({ force: reset });
12
+ console.log(reset ? "Database reset — tables recreated." : "Database synced — tables ensured.");
13
+ await sequelize.close();
@@ -0,0 +1,25 @@
1
+ import "dotenv/config";
2
+
3
+ import { assertConnection } from "@softeneers/db";
4
+
5
+ import { carStore } from "../cars/store.js";
6
+ import { sequelize } from "../db.js";
7
+
8
+ await assertConnection(sequelize);
9
+ await sequelize.sync();
10
+
11
+ const existing = await carStore.list();
12
+ if (existing.length > 0) {
13
+ console.log(`Skipped seeding — ${existing.length} cars already present.`);
14
+ } else {
15
+ for (const car of [
16
+ { brand: "Toyota", model: "Corolla", year: 2021 },
17
+ { brand: "Tesla", model: "Model 3", year: 2023 },
18
+ { brand: "Ford", model: "Mustang", year: 1969 },
19
+ ]) {
20
+ await carStore.create(car);
21
+ }
22
+ console.log("Seeded 3 cars.");
23
+ }
24
+
25
+ await sequelize.close();
@@ -0,0 +1,25 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { parseNewCar, ValidationError } from "../src/cars/validate.ts";
5
+
6
+ test("parses a valid car body", () => {
7
+ assert.deepEqual(parseNewCar({ brand: "Tesla", model: "Model 3", year: 2023 }), {
8
+ brand: "Tesla",
9
+ model: "Model 3",
10
+ year: 2023,
11
+ });
12
+ });
13
+
14
+ test("trims strings and coerces a numeric year", () => {
15
+ assert.deepEqual(parseNewCar({ brand: " Ford ", model: " Mustang ", year: "1969" }), {
16
+ brand: "Ford",
17
+ model: "Mustang",
18
+ year: 1969,
19
+ });
20
+ });
21
+
22
+ test("rejects an incomplete body", () => {
23
+ assert.throws(() => parseNewCar({ brand: "Toyota" }), ValidationError);
24
+ assert.throws(() => parseNewCar({}), ValidationError);
25
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "rootDir": "src",
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src"]
14
+ }
@@ -0,0 +1,13 @@
1
+ PORT=4000
2
+ CORS_ORIGIN=http://localhost:3000
3
+ # #if db
4
+ DB_HOST=127.0.0.1
5
+ DB_PORT=3306
6
+ DB_NAME=app_dev
7
+ DB_USER=root
8
+ DB_PASSWORD=
9
+ # #endif
10
+ # #if auth
11
+ AUTH_SECRET=dev-secret-change-me-to-a-long-random-string
12
+ AUTH_BASE_URL=http://localhost:4000
13
+ # #endif
@@ -0,0 +1,77 @@
1
+ # {{PROJECT_NAME}}
2
+
3
+ A **Hono + TypeScript** API generated by
4
+ [`create-softeneers-app`](https://www.npmjs.com/package/create-softeneers-app),
5
+ served on Node via `@hono/node-server`, with a full cars CRUD example.
6
+
7
+ ## Scripts
8
+
9
+ ```bash
10
+ npm run dev # watch-run src/index.ts (tsx)
11
+ npm run build # type-check + emit dist/ (tsc)
12
+ npm start # run the built server
13
+ npm run typecheck # tsc --noEmit
14
+ npm test # node:test suite (via tsx)
15
+ # #if db
16
+ npm run db:migrate # create/sync tables
17
+ npm run db:seed # insert sample cars
18
+ npm run db:reset # drop, recreate, reseed
19
+ # #endif
20
+ ```
21
+
22
+ ## API
23
+
24
+ | Method | Path | Description |
25
+ | ------ | ---------------- | ------------------ |
26
+ | GET | `/health` | Liveness check |
27
+ | GET | `/api/cars` | List cars |
28
+ | GET | `/api/cars/:id` | Get one car |
29
+ | POST | `/api/cars` | Create a car |
30
+ | PUT | `/api/cars/:id` | Update a car |
31
+ | DELETE | `/api/cars/:id` | Delete a car |
32
+ # #if auth
33
+ | GET/POST | `/api/auth/*` | better-auth routes |
34
+ # #endif
35
+
36
+ ```bash
37
+ curl localhost:4000/api/cars
38
+ curl -X POST localhost:4000/api/cars -H 'content-type: application/json' \
39
+ -d '{"brand":"Tesla","model":"Model 3","year":2023}'
40
+ ```
41
+ # #if db
42
+
43
+ ## Database
44
+
45
+ Persistence is MySQL via Sequelize ([`@softeneers/db`](https://www.npmjs.com/package/@softeneers/db)).
46
+ Configure `DB_*` in `.env`, then:
47
+
48
+ ```bash
49
+ # #if docker
50
+ docker compose up -d # start MySQL
51
+ # #endif
52
+ npm run db:migrate && npm run db:seed
53
+ ```
54
+
55
+ Generate with `--no-db` to swap in a dependency-free in-memory store — the CRUD
56
+ API behaves identically.
57
+ # #endif
58
+ # #if auth
59
+
60
+ ## Authentication
61
+
62
+ Email + password auth via better-auth ([`@softeneers/auth`](https://www.npmjs.com/package/@softeneers/auth)),
63
+ mounted at `/api/auth/*`. Set a strong `AUTH_SECRET` in `.env` before deploying.
64
+ # #endif
65
+
66
+ ## Getting started
67
+
68
+ ```bash
69
+ npm install
70
+ # #if docker
71
+ docker compose up -d
72
+ # #endif
73
+ # #if db
74
+ npm run db:migrate && npm run db:seed
75
+ # #endif
76
+ npm run dev
77
+ ```
@@ -0,0 +1,15 @@
1
+ services:
2
+ db:
3
+ image: mysql:8
4
+ restart: unless-stopped
5
+ environment:
6
+ MYSQL_DATABASE: ${DB_NAME:-app_dev}
7
+ MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:-}
8
+ MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
9
+ ports:
10
+ - "${DB_PORT:-3306}:3306"
11
+ volumes:
12
+ - db_data:/var/lib/mysql
13
+
14
+ volumes:
15
+ db_data:
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "hono-api",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Hono + TypeScript API generated by create-softeneers-app.",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "scripts": {
11
+ "dev": "tsx watch src/index.ts",
12
+ "build": "tsc",
13
+ "start": "node dist/index.js",
14
+ "typecheck": "tsc --noEmit",
15
+ "db:migrate": "tsx src/scripts/migrate.ts",
16
+ "db:seed": "tsx src/scripts/seed.ts",
17
+ "db:reset": "tsx src/scripts/migrate.ts --reset && tsx src/scripts/seed.ts",
18
+ "test": "node --import tsx --test test/*.test.ts"
19
+ },
20
+ "dependencies": {
21
+ "@hono/node-server": "^1.13.0",
22
+ "@softeneers/auth": "^0.1.0",
23
+ "@softeneers/db": "^0.1.0",
24
+ "@softeneers/env": "^0.1.0",
25
+ "dotenv": "^16.4.5",
26
+ "hono": "^4.6.0",
27
+ "mysql2": "^3.11.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.0.0",
31
+ "tsx": "^4.19.0",
32
+ "typescript": "^5.6.0"
33
+ }
34
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "toggles": { "db": true, "auth": false, "docker": true },
3
+ "fragments": {
4
+ "db": {
5
+ "removePaths": ["src/db.ts", "src/scripts", "docker-compose.yml"],
6
+ "removeDeps": ["@softeneers/db", "mysql2"],
7
+ "removeScripts": ["db:migrate", "db:seed", "db:reset"]
8
+ },
9
+ "auth": {
10
+ "removePaths": ["src/auth"],
11
+ "removeDeps": ["@softeneers/auth"]
12
+ },
13
+ "docker": {
14
+ "removePaths": ["docker-compose.yml"]
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,11 @@
1
+ import { createAuth } from "@softeneers/auth";
2
+
3
+ import { env } from "../env.js";
4
+
5
+ // Email + password authentication via better-auth. With no `database` configured
6
+ // better-auth uses an in-memory store (fine for local dev); point it at your DB
7
+ // for persistence. Routes are mounted at /api/auth/* in src/index.ts.
8
+ export const auth = createAuth({
9
+ secret: env.AUTH_SECRET,
10
+ baseURL: env.AUTH_BASE_URL,
11
+ });
@@ -0,0 +1,43 @@
1
+ import { Hono } from "hono";
2
+
3
+ import { carStore } from "./store.js";
4
+ import { parseNewCar, ValidationError } from "./validate.js";
5
+
6
+ export const cars = new Hono();
7
+
8
+ cars.get("/", async (c) => {
9
+ return c.json(await carStore.list());
10
+ });
11
+
12
+ cars.get("/:id", async (c) => {
13
+ const car = await carStore.get(Number(c.req.param("id")));
14
+ if (!car) return c.json({ message: "Car not found." }, 404);
15
+ return c.json(car);
16
+ });
17
+
18
+ cars.post("/", async (c) => {
19
+ try {
20
+ const car = await carStore.create(parseNewCar(await c.req.json()));
21
+ return c.json(car, 201);
22
+ } catch (error) {
23
+ if (error instanceof ValidationError) return c.json({ message: error.message }, 400);
24
+ throw error;
25
+ }
26
+ });
27
+
28
+ cars.put("/:id", async (c) => {
29
+ try {
30
+ const car = await carStore.update(Number(c.req.param("id")), parseNewCar(await c.req.json()));
31
+ if (!car) return c.json({ message: "Car not found." }, 404);
32
+ return c.json(car);
33
+ } catch (error) {
34
+ if (error instanceof ValidationError) return c.json({ message: error.message }, 400);
35
+ throw error;
36
+ }
37
+ });
38
+
39
+ cars.delete("/:id", async (c) => {
40
+ const removed = await carStore.remove(Number(c.req.param("id")));
41
+ if (!removed) return c.json({ message: "Car not found." }, 404);
42
+ return c.body(null, 204);
43
+ });