@bryanchu10/create-template-ts 1.2.1 → 1.3.1

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 (39) hide show
  1. package/README.md +1 -0
  2. package/README.zh-TW.md +1 -0
  3. package/dist/index.js +41 -1
  4. package/package.json +1 -1
  5. package/templates/hono/.env.example +4 -0
  6. package/templates/hono/_gitignore +11 -0
  7. package/templates/hono/_vscode/settings.json +40 -0
  8. package/templates/hono/drizzle.config.ts +11 -0
  9. package/templates/hono/eslint.config.js +49 -0
  10. package/templates/hono/package.json +16 -0
  11. package/templates/hono/pnpm-workspace.yaml +5 -0
  12. package/templates/hono/rolldown.config.ts +10 -0
  13. package/templates/hono/src/app.ts +59 -0
  14. package/templates/hono/src/config/env.ts +14 -0
  15. package/templates/hono/src/db/client.ts +10 -0
  16. package/templates/hono/src/db/schema.ts +35 -0
  17. package/templates/hono/src/features/users/__tests__/user.router.test.ts +166 -0
  18. package/templates/hono/src/features/users/__tests__/user.service.test.ts +103 -0
  19. package/templates/hono/src/features/users/index.ts +1 -0
  20. package/templates/hono/src/features/users/user.errors.ts +19 -0
  21. package/templates/hono/src/features/users/user.handler.ts +10 -0
  22. package/templates/hono/src/features/users/user.repo.ts +95 -0
  23. package/templates/hono/src/features/users/user.router.ts +210 -0
  24. package/templates/hono/src/features/users/user.schema.ts +54 -0
  25. package/templates/hono/src/features/users/user.service.ts +35 -0
  26. package/templates/hono/src/index.ts +59 -0
  27. package/templates/hono/src/lib/logger.ts +16 -0
  28. package/templates/hono/src/middleware/error-handler.ts +8 -0
  29. package/templates/hono/tsconfig.json +15 -0
  30. package/templates/hono/vitest.config.ts +17 -0
  31. package/templates/ts-library/_gitignore +1 -1
  32. package/templates/ts-library/_vscode/settings.json +5 -1
  33. package/templates/ts-library/eslint.config.js +7 -1
  34. package/templates/ts-library/tsconfig.json +4 -1
  35. package/templates/ts-library/vitest.config.ts +12 -0
  36. package/templates/ts-script/_gitignore +1 -1
  37. package/templates/ts-script/_vscode/settings.json +5 -1
  38. package/templates/ts-script/eslint.config.js +1 -1
  39. package/templates/ts-script/tsconfig.json +4 -1
package/README.md CHANGED
@@ -30,6 +30,7 @@ Follow the prompts to choose a project name and template. The CLI will fetch the
30
30
  |----------|-------------|
31
31
  | `ts-script` | TypeScript script/tooling with neverthrow, ts-pattern, eslint, tsx, and rolldown |
32
32
  | `ts-library` | TypeScript library with tsdown, vitest, eslint, and release-it |
33
+ | `hono` | Hono API server with Drizzle ORM (SQLite), OpenAPI docs via Scalar, pino logging, and neverthrow |
33
34
 
34
35
  ## Development
35
36
 
package/README.zh-TW.md CHANGED
@@ -30,6 +30,7 @@ npx @bryanchu10/create-template-ts
30
30
  |------|------|
31
31
  | `ts-script` | TypeScript 腳本/工具專案,內含 neverthrow、ts-pattern、eslint、tsx、rolldown |
32
32
  | `ts-library` | TypeScript 函式庫專案,內含 tsdown、vitest、eslint、release-it |
33
+ | `hono` | Hono API 伺服器,內含 Drizzle ORM(SQLite)、Scalar OpenAPI 文件、pino 日誌、neverthrow |
33
34
 
34
35
  ## 開發
35
36
 
package/dist/index.js CHANGED
@@ -1487,6 +1487,35 @@ const TEMPLATES = {
1487
1487
  "release-it",
1488
1488
  "@release-it/conventional-changelog"
1489
1489
  ]
1490
+ },
1491
+ "hono": {
1492
+ hint: "Hono API server with Drizzle ORM, OpenAPI & SQLite",
1493
+ withPeerDependencies: false,
1494
+ deps: [
1495
+ "@hono/node-server",
1496
+ "@hono/zod-openapi",
1497
+ "@scalar/hono-api-reference",
1498
+ "better-sqlite3",
1499
+ "drizzle-orm",
1500
+ "hono",
1501
+ "neverthrow",
1502
+ "pino",
1503
+ "pino-http",
1504
+ "ts-pattern",
1505
+ "zod"
1506
+ ],
1507
+ devDeps: [
1508
+ "@antfu/eslint-config",
1509
+ "@types/better-sqlite3",
1510
+ "@vitest/coverage-v8",
1511
+ "drizzle-kit",
1512
+ "eslint",
1513
+ "pino-pretty",
1514
+ "rolldown",
1515
+ "tsx",
1516
+ "typescript",
1517
+ "vitest"
1518
+ ]
1490
1519
  }
1491
1520
  };
1492
1521
  //#endregion
@@ -1983,7 +2012,15 @@ function resolveNewDir(projectName) {
1983
2012
  pkg.dependencies = depsMap;
1984
2013
  Object.assign(pkg, withPeerDependencies ? { peerDependencies: Object.fromEntries(Object.entries(depsMap).map(([dep, ver]) => [dep, ver.replace(/^(\^?\d+)\..*$/, "$1")])) } : {});
1985
2014
  pkg.devDependencies = devDepsMap;
1986
- return safeWriteFileSync(pkgPath, `${JSON.stringify(pkg, null, 4)}\n`);
2015
+ const { name, dependencies, peerDependencies, devDependencies, ...rest } = pkg;
2016
+ const ordered = {
2017
+ name,
2018
+ ...rest,
2019
+ dependencies: sortKeys(dependencies),
2020
+ ...peerDependencies ? { peerDependencies: sortKeys(peerDependencies) } : {},
2021
+ devDependencies: sortKeys(devDependencies)
2022
+ };
2023
+ return safeWriteFileSync(pkgPath, `${JSON.stringify(ordered, null, 4)}\n`);
1987
2024
  }).map(() => projectName);
1988
2025
  }).match((projectName) => ye(`Done! Run:\n\n cd ${projectName}\n pnpm install`), (err) => {
1989
2026
  me(err.message);
@@ -2008,5 +2045,8 @@ function safeWriteFileSync(path, data) {
2008
2045
  function safeJsonParse(text) {
2009
2046
  return (0, import_index_cjs.fromThrowable)((t) => JSON.parse(t), (e) => e)(text);
2010
2047
  }
2048
+ function sortKeys(obj) {
2049
+ return obj && Object.fromEntries(Object.entries(obj).sort(([a], [b]) => a.localeCompare(b)));
2050
+ }
2011
2051
  //#endregion
2012
2052
  export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bryanchu10/create-template-ts",
3
3
  "type": "module",
4
- "version": "1.2.1",
4
+ "version": "1.3.1",
5
5
  "description": "Scaffold a modular TypeScript project for scripts and tooling",
6
6
  "author": "Bryan Chu <bryanchu10@gmail.com> (https://github.com/bryanchu10)",
7
7
  "license": "MIT",
@@ -0,0 +1,4 @@
1
+ NODE_ENV=development
2
+ PORT=3000
3
+ DATABASE_URL=./data.db
4
+ LOG_LEVEL=info
@@ -0,0 +1,11 @@
1
+ node_modules
2
+ dist
3
+ .DS_Store
4
+
5
+ # database
6
+ *.db
7
+ *.db-shm
8
+ *.db-wal
9
+
10
+ # environment
11
+ .env
@@ -0,0 +1,40 @@
1
+ {
2
+ // Disable the default formatter, use eslint instead
3
+ "prettier.enable": false,
4
+ "editor.formatOnSave": false,
5
+
6
+ // Auto fix
7
+ "editor.codeActionsOnSave": {
8
+ "source.fixAll.eslint": "explicit",
9
+ "source.organizeImports": "never"
10
+ },
11
+
12
+ // Enable eslint for all supported languages
13
+ "eslint.validate": [
14
+ "javascript",
15
+ "javascriptreact",
16
+ "typescript",
17
+ "typescriptreact",
18
+ "vue",
19
+ "html",
20
+ "markdown",
21
+ "json",
22
+ "jsonc",
23
+ "yaml",
24
+ "toml",
25
+ "xml",
26
+ "gql",
27
+ "graphql",
28
+ "astro",
29
+ "svelte",
30
+ "css",
31
+ "less",
32
+ "scss",
33
+ "pcss",
34
+ "postcss"
35
+ ],
36
+
37
+ // Use workspace TypeScript so that editor diagnostics are consistent with the project compiler.
38
+ // If prompted, run "TypeScript: Select TypeScript Version" and choose "Use Workspace Version".
39
+ "js/ts.tsdk.path": "node_modules/typescript/lib"
40
+ }
@@ -0,0 +1,11 @@
1
+ import process from "node:process";
2
+ import { defineConfig } from "drizzle-kit";
3
+
4
+ export default defineConfig({
5
+ schema: "./src/db/schema.ts",
6
+ out: "./drizzle",
7
+ dialect: "sqlite",
8
+ dbCredentials: {
9
+ url: process.env.DATABASE_URL ?? "./data.db",
10
+ },
11
+ });
@@ -0,0 +1,49 @@
1
+ import antfu from "@antfu/eslint-config";
2
+
3
+ export default antfu(
4
+ {
5
+ ignores: ["drizzle/**"],
6
+ typescript: true,
7
+ stylistic: {
8
+ indent: 4,
9
+ quotes: "double",
10
+ braceStyle: "stroustrup",
11
+ semi: true,
12
+ overrides: {
13
+ "style/jsx-one-expression-per-line": ["error", { allow: "non-jsx" }],
14
+ "style/jsx-first-prop-new-line": ["error", "never"],
15
+ "style/jsx-max-props-per-line": "off",
16
+ "style/jsx-closing-bracket-location": ["error", "after-props"],
17
+ "style/operator-linebreak": ["error", "before", { overrides: { "=": "after" } }],
18
+ "style/padding-line-between-statements": [
19
+ "error",
20
+ { blankLine: "always", prev: "*", next: "for" },
21
+ { blankLine: "always", prev: "for", next: "*" },
22
+ { blankLine: "always", prev: "*", next: "return" },
23
+ ],
24
+ },
25
+ },
26
+ },
27
+ {
28
+ rules: {
29
+ "new-cap": ["error", {
30
+ capIsNew: true,
31
+ newIsCap: true,
32
+ properties: true,
33
+ }],
34
+ "no-shadow": "error",
35
+ },
36
+ },
37
+ {
38
+ files: ["pnpm-workspace.yaml"],
39
+ rules: {
40
+ "pnpm/yaml-enforce-settings": ["error", { settings: { shellEmulator: true } }],
41
+ },
42
+ },
43
+ {
44
+ files: ["**/*.yml", "**/*.yaml"],
45
+ rules: {
46
+ "yaml/indent": ["error", 2],
47
+ },
48
+ },
49
+ );
@@ -0,0 +1,16 @@
1
+ {
2
+ "type": "module",
3
+ "scripts": {
4
+ "dev": "tsx watch src/index.ts",
5
+ "check": "tsc --noEmit",
6
+ "build": "rolldown -c",
7
+ "lint": "eslint",
8
+ "lint:fix": "eslint --fix",
9
+ "db:generate": "drizzle-kit generate",
10
+ "db:migrate": "drizzle-kit migrate",
11
+ "db:studio": "drizzle-kit studio",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "test:coverage": "vitest run --coverage"
15
+ }
16
+ }
@@ -0,0 +1,5 @@
1
+ shellEmulator: true
2
+
3
+ allowBuilds:
4
+ better-sqlite3: true
5
+ esbuild: true
@@ -0,0 +1,10 @@
1
+ export default {
2
+ input: ["src/index.ts"],
3
+ platform: "node",
4
+ external: ["better-sqlite3", "pino", "pino-pretty", "pino-http"],
5
+ resolve: {
6
+ alias: {
7
+ "@": "./src",
8
+ },
9
+ },
10
+ };
@@ -0,0 +1,59 @@
1
+ import type { DB } from "./db/client";
2
+ import { OpenAPIHono } from "@hono/zod-openapi";
3
+ import { Scalar as scalar } from "@scalar/hono-api-reference";
4
+ import { env } from "./config/env";
5
+ import { db } from "./db/client";
6
+ import { createUserRouter } from "./features/users/index";
7
+ import { UserRepository } from "./features/users/user.repo";
8
+ import { UserService } from "./features/users/user.service";
9
+ import { errorHandler } from "./middleware/error-handler";
10
+
11
+ export function createApp(dbOverride?: DB) {
12
+ const app = new OpenAPIHono({
13
+ defaultHook(result, c) {
14
+ if (!result.success) {
15
+ const message = result.error.issues
16
+ .map(i => i.message)
17
+ .join(", ");
18
+
19
+ return c.json({ message }, 422);
20
+ }
21
+ },
22
+ });
23
+
24
+ const database = dbOverride ?? db;
25
+ const userRepo = new UserRepository(database);
26
+ const userService = new UserService(userRepo);
27
+
28
+ app.route("/api/users", createUserRouter(userService));
29
+
30
+ app.doc31("/api/openapi.json", {
31
+ openapi: "3.1.0",
32
+ info: {
33
+ title: "Hono API Demo",
34
+ version: "1.0.0",
35
+ description: "A layered Hono API server with OpenAPI documentation",
36
+ },
37
+ });
38
+
39
+ if (env.NODE_ENV !== "production") {
40
+ app.get(
41
+ "/docs",
42
+ scalar({
43
+ url: "/api/openapi.json",
44
+ pageTitle: "Hono API Docs",
45
+ showDeveloperTools: "never",
46
+ mcp: { disabled: true },
47
+ agent: {
48
+ disabled: true,
49
+ },
50
+ hideClientButton: true,
51
+ telemetry: false,
52
+ }),
53
+ );
54
+ }
55
+
56
+ app.onError(errorHandler);
57
+
58
+ return app;
59
+ }
@@ -0,0 +1,14 @@
1
+ import process from "node:process";
2
+ import { z } from "zod";
3
+
4
+ const envSchema = z.object({
5
+ NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
6
+ PORT: z.coerce.number().int().positive().default(3000),
7
+ DATABASE_URL: z.string().min(1).default("./data.db"),
8
+ LOG_LEVEL: z
9
+ .enum(["fatal", "error", "warn", "info", "debug", "trace"])
10
+ .default("info"),
11
+ });
12
+
13
+ export const env = envSchema.parse(process.env);
14
+ export type Env = typeof env;
@@ -0,0 +1,10 @@
1
+ import Database from "better-sqlite3";
2
+ import { drizzle } from "drizzle-orm/better-sqlite3";
3
+ import { env } from "../config/env";
4
+ import * as schema from "./schema";
5
+
6
+ const sqlite = new Database(env.DATABASE_URL);
7
+ sqlite.pragma("foreign_keys = ON");
8
+
9
+ export const db = drizzle(sqlite, { schema });
10
+ export type DB = typeof db;
@@ -0,0 +1,35 @@
1
+ import { relations } from "drizzle-orm";
2
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
3
+
4
+ export const users = sqliteTable("users", {
5
+ id: text("id")
6
+ .primaryKey()
7
+ .$defaultFn(() => crypto.randomUUID()),
8
+ name: text("name").notNull(),
9
+ email: text("email").notNull().unique(),
10
+ createdAt: integer("created_at", { mode: "timestamp" })
11
+ .notNull()
12
+ .$defaultFn(() => new Date()),
13
+ });
14
+
15
+ export const posts = sqliteTable("posts", {
16
+ id: text("id")
17
+ .primaryKey()
18
+ .$defaultFn(() => crypto.randomUUID()),
19
+ title: text("title").notNull(),
20
+ content: text("content").notNull(),
21
+ userId: text("user_id")
22
+ .notNull()
23
+ .references(() => users.id, { onDelete: "cascade" }),
24
+ createdAt: integer("created_at", { mode: "timestamp" })
25
+ .notNull()
26
+ .$defaultFn(() => new Date()),
27
+ });
28
+
29
+ export const usersRelations = relations(users, ({ many }) => ({
30
+ posts: many(posts),
31
+ }));
32
+
33
+ export const postsRelations = relations(posts, ({ one }) => ({
34
+ user: one(users, { fields: [posts.userId], references: [users.id] }),
35
+ }));
@@ -0,0 +1,166 @@
1
+ import Database from "better-sqlite3";
2
+ import { drizzle } from "drizzle-orm/better-sqlite3";
3
+ import { beforeEach, describe, expect, it } from "vitest";
4
+ import { createApp } from "../../../app";
5
+ import * as schema from "../../../db/schema";
6
+
7
+ function setupTestDb() {
8
+ const sqlite = new Database(":memory:");
9
+ sqlite.pragma("foreign_keys = ON");
10
+ sqlite.exec(`
11
+ CREATE TABLE users (
12
+ id TEXT PRIMARY KEY NOT NULL,
13
+ name TEXT NOT NULL,
14
+ email TEXT NOT NULL UNIQUE,
15
+ created_at INTEGER NOT NULL
16
+ )
17
+ `);
18
+ sqlite.exec(`
19
+ CREATE TABLE posts (
20
+ id TEXT PRIMARY KEY NOT NULL,
21
+ title TEXT NOT NULL,
22
+ content TEXT NOT NULL,
23
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
24
+ created_at INTEGER NOT NULL
25
+ )
26
+ `);
27
+
28
+ return drizzle(sqlite, { schema });
29
+ }
30
+
31
+ describe("user API", () => {
32
+ let app: ReturnType<typeof createApp>;
33
+
34
+ beforeEach(() => {
35
+ app = createApp(setupTestDb());
36
+ });
37
+
38
+ describe("gET /api/users", () => {
39
+ it("returns empty array initially", async () => {
40
+ const res = await app.request("/api/users");
41
+ expect(res.status).toBe(200);
42
+ expect(await res.json()).toEqual([]);
43
+ });
44
+ });
45
+
46
+ describe("pOST /api/users", () => {
47
+ it("creates a user and returns 201", async () => {
48
+ const res = await app.request("/api/users", {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify({ name: "Alice", email: "alice@example.com" }),
52
+ });
53
+ expect(res.status).toBe(201);
54
+ const body = await res.json() as { id: string; name: string; email: string };
55
+ expect(body.name).toBe("Alice");
56
+ expect(body.email).toBe("alice@example.com");
57
+ expect(body.id).toBeTypeOf("string");
58
+ });
59
+
60
+ it("returns 422 for missing email", async () => {
61
+ const res = await app.request("/api/users", {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
64
+ body: JSON.stringify({ name: "Alice" }),
65
+ });
66
+ expect(res.status).toBe(422);
67
+ });
68
+
69
+ it("returns 409 for duplicate email", async () => {
70
+ const payload = JSON.stringify({ name: "Alice", email: "alice@example.com" });
71
+ await app.request("/api/users", {
72
+ method: "POST",
73
+ headers: { "Content-Type": "application/json" },
74
+ body: payload,
75
+ });
76
+ const res = await app.request("/api/users", {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: payload,
80
+ });
81
+ expect(res.status).toBe(409);
82
+ });
83
+ });
84
+
85
+ describe("gET /api/users/:id", () => {
86
+ it("returns 404 for unknown id", async () => {
87
+ const res = await app.request(
88
+ "/api/users/00000000-0000-0000-0000-000000000000",
89
+ );
90
+ expect(res.status).toBe(404);
91
+ });
92
+
93
+ it("returns the user by id", async () => {
94
+ const createRes = await app.request("/api/users", {
95
+ method: "POST",
96
+ headers: { "Content-Type": "application/json" },
97
+ body: JSON.stringify({ name: "Bob", email: "bob@example.com" }),
98
+ });
99
+ const { id } = await createRes.json() as { id: string };
100
+
101
+ const res = await app.request(`/api/users/${id}`);
102
+ expect(res.status).toBe(200);
103
+ const body = await res.json() as { name: string };
104
+ expect(body.name).toBe("Bob");
105
+ });
106
+ });
107
+
108
+ describe("pATCH /api/users/:id", () => {
109
+ it("updates a user name", async () => {
110
+ const createRes = await app.request("/api/users", {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({ name: "Carol", email: "carol@example.com" }),
114
+ });
115
+ const { id } = await createRes.json() as { id: string };
116
+
117
+ const res = await app.request(`/api/users/${id}`, {
118
+ method: "PATCH",
119
+ headers: { "Content-Type": "application/json" },
120
+ body: JSON.stringify({ name: "Carol Updated" }),
121
+ });
122
+ expect(res.status).toBe(200);
123
+ const body = await res.json() as { name: string };
124
+ expect(body.name).toBe("Carol Updated");
125
+ });
126
+
127
+ it("returns 404 for unknown id", async () => {
128
+ const res = await app.request(
129
+ "/api/users/00000000-0000-0000-0000-000000000000",
130
+ {
131
+ method: "PATCH",
132
+ headers: { "Content-Type": "application/json" },
133
+ body: JSON.stringify({ name: "Nobody" }),
134
+ },
135
+ );
136
+ expect(res.status).toBe(404);
137
+ });
138
+ });
139
+
140
+ describe("dELETE /api/users/:id", () => {
141
+ it("deletes a user and returns 204", async () => {
142
+ const createRes = await app.request("/api/users", {
143
+ method: "POST",
144
+ headers: { "Content-Type": "application/json" },
145
+ body: JSON.stringify({ name: "Dave", email: "dave@example.com" }),
146
+ });
147
+ const { id } = await createRes.json() as { id: string };
148
+
149
+ const deleteRes = await app.request(`/api/users/${id}`, {
150
+ method: "DELETE",
151
+ });
152
+ expect(deleteRes.status).toBe(204);
153
+
154
+ const getRes = await app.request(`/api/users/${id}`);
155
+ expect(getRes.status).toBe(404);
156
+ });
157
+
158
+ it("returns 404 for unknown id", async () => {
159
+ const res = await app.request(
160
+ "/api/users/00000000-0000-0000-0000-000000000000",
161
+ { method: "DELETE" },
162
+ );
163
+ expect(res.status).toBe(404);
164
+ });
165
+ });
166
+ });
@@ -0,0 +1,103 @@
1
+ import type { Mock } from "vitest";
2
+ import type { UserRepository } from "../user.repo";
3
+ import { err, ok } from "neverthrow";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { UserService } from "../user.service";
6
+
7
+ const now = new Date();
8
+ const fakeUser = {
9
+ id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
10
+ name: "Alice",
11
+ email: "alice@example.com",
12
+ createdAt: now,
13
+ };
14
+
15
+ function mockRepo(): UserRepository {
16
+ return {
17
+ findAll: vi.fn(),
18
+ findById: vi.fn(),
19
+ create: vi.fn(),
20
+ update: vi.fn(),
21
+ deleteById: vi.fn(),
22
+ } as unknown as UserRepository;
23
+ }
24
+
25
+ describe("userService", () => {
26
+ describe("listUsers", () => {
27
+ it("returns users from repo", () => {
28
+ const repo = mockRepo();
29
+ (repo.findAll as Mock).mockReturnValue(ok([fakeUser]));
30
+
31
+ const result = new UserService(repo).listUsers();
32
+
33
+ expect(result.isOk()).toBe(true);
34
+ expect(result._unsafeUnwrap()).toEqual([fakeUser]);
35
+ });
36
+
37
+ it("propagates database error", () => {
38
+ const repo = mockRepo();
39
+ (repo.findAll as Mock).mockReturnValue(
40
+ err({ type: "USER_DATABASE_ERROR", cause: new Error("DB down") }),
41
+ );
42
+
43
+ const result = new UserService(repo).listUsers();
44
+
45
+ expect(result.isErr()).toBe(true);
46
+ expect(result._unsafeUnwrapErr().type).toBe("USER_DATABASE_ERROR");
47
+ });
48
+ });
49
+
50
+ describe("getUserById", () => {
51
+ it("returns user when found", () => {
52
+ const repo = mockRepo();
53
+ (repo.findById as Mock).mockReturnValue(ok(fakeUser));
54
+
55
+ const result = new UserService(repo).getUserById(fakeUser.id);
56
+
57
+ expect(result.isOk()).toBe(true);
58
+ expect(result._unsafeUnwrap()).toEqual(fakeUser);
59
+ });
60
+
61
+ it("returns USER_NOT_FOUND when missing", () => {
62
+ const repo = mockRepo();
63
+ (repo.findById as Mock).mockReturnValue(
64
+ err({ type: "USER_NOT_FOUND", id: "missing-id" }),
65
+ );
66
+
67
+ const result = new UserService(repo).getUserById("missing-id");
68
+
69
+ expect(result.isErr()).toBe(true);
70
+ expect(result._unsafeUnwrapErr()).toMatchObject({ type: "USER_NOT_FOUND" });
71
+ });
72
+ });
73
+
74
+ describe("createUser", () => {
75
+ it("creates and returns new user", () => {
76
+ const repo = mockRepo();
77
+ (repo.create as Mock).mockReturnValue(ok(fakeUser));
78
+
79
+ const result = new UserService(repo).createUser({
80
+ name: "Alice",
81
+ email: "alice@example.com",
82
+ });
83
+
84
+ expect(result.isOk()).toBe(true);
85
+ expect(result._unsafeUnwrap()).toEqual(fakeUser);
86
+ });
87
+
88
+ it("returns USER_DUPLICATE_EMAIL on conflict", () => {
89
+ const repo = mockRepo();
90
+ (repo.create as Mock).mockReturnValue(
91
+ err({ type: "USER_DUPLICATE_EMAIL", email: "alice@example.com" }),
92
+ );
93
+
94
+ const result = new UserService(repo).createUser({
95
+ name: "Alice",
96
+ email: "alice@example.com",
97
+ });
98
+
99
+ expect(result.isErr()).toBe(true);
100
+ expect(result._unsafeUnwrapErr()).toMatchObject({ type: "USER_DUPLICATE_EMAIL" });
101
+ });
102
+ });
103
+ });
@@ -0,0 +1 @@
1
+ export { createUserRouter } from "./user.router";
@@ -0,0 +1,19 @@
1
+ export interface UserNotFoundError {
2
+ type: "USER_NOT_FOUND";
3
+ id: string;
4
+ }
5
+
6
+ export interface UserDuplicateEmailError {
7
+ type: "USER_DUPLICATE_EMAIL";
8
+ email: string;
9
+ }
10
+
11
+ export interface UserDatabaseError {
12
+ type: "USER_DATABASE_ERROR";
13
+ cause: unknown;
14
+ }
15
+
16
+ export type UserError =
17
+ | UserNotFoundError
18
+ | UserDuplicateEmailError
19
+ | UserDatabaseError;
@@ -0,0 +1,10 @@
1
+ import type { UserRow } from "./user.repo";
2
+
3
+ export function toUserResponse(user: UserRow) {
4
+ return {
5
+ id: user.id,
6
+ name: user.name,
7
+ email: user.email,
8
+ createdAt: user.createdAt.toISOString(),
9
+ };
10
+ }
@@ -0,0 +1,95 @@
1
+ import type { Result } from "neverthrow";
2
+ import type { DB } from "../../db/client";
3
+ import type {
4
+ UserDatabaseError,
5
+ UserDuplicateEmailError,
6
+ UserNotFoundError,
7
+ } from "./user.errors";
8
+ import { eq } from "drizzle-orm";
9
+ import { err, ok } from "neverthrow";
10
+ import { users } from "../../db/schema";
11
+
12
+ export type UserRow = typeof users.$inferSelect;
13
+ export type NewUser = typeof users.$inferInsert;
14
+
15
+ function isSQLiteUniqueViolation(error: unknown): boolean {
16
+ return error instanceof Error && error.message.includes("UNIQUE constraint failed");
17
+ }
18
+
19
+ export class UserRepository {
20
+ constructor(private readonly db: DB) {}
21
+
22
+ findAll(): Result<UserRow[], UserDatabaseError> {
23
+ try {
24
+ return ok(this.db.select().from(users).all());
25
+ }
26
+ catch (cause) {
27
+ return err({ type: "USER_DATABASE_ERROR", cause });
28
+ }
29
+ }
30
+
31
+ findById(id: string): Result<UserRow, UserNotFoundError | UserDatabaseError> {
32
+ try {
33
+ const user = this.db.select().from(users).where(eq(users.id, id)).get();
34
+ if (!user)
35
+ return err({ type: "USER_NOT_FOUND", id });
36
+
37
+ return ok(user);
38
+ }
39
+ catch (cause) {
40
+ return err({ type: "USER_DATABASE_ERROR", cause });
41
+ }
42
+ }
43
+
44
+ create(data: NewUser): Result<UserRow, UserDuplicateEmailError | UserDatabaseError> {
45
+ try {
46
+ const rows = this.db.insert(users).values(data).returning().all();
47
+ const user = rows[0];
48
+ if (!user)
49
+ return err({ type: "USER_DATABASE_ERROR", cause: new Error("Insert returned no rows") });
50
+
51
+ return ok(user);
52
+ }
53
+ catch (cause) {
54
+ if (isSQLiteUniqueViolation(cause)) {
55
+ return err({ type: "USER_DUPLICATE_EMAIL", email: data.email! });
56
+ }
57
+
58
+ return err({ type: "USER_DATABASE_ERROR", cause });
59
+ }
60
+ }
61
+
62
+ update(
63
+ id: string,
64
+ data: Partial<Omit<NewUser, "id">>,
65
+ ): Result<UserRow, UserNotFoundError | UserDuplicateEmailError | UserDatabaseError> {
66
+ try {
67
+ const rows = this.db.update(users).set(data).where(eq(users.id, id)).returning().all();
68
+ const user = rows[0];
69
+ if (!user)
70
+ return err({ type: "USER_NOT_FOUND", id });
71
+
72
+ return ok(user);
73
+ }
74
+ catch (cause) {
75
+ if (isSQLiteUniqueViolation(cause)) {
76
+ return err({ type: "USER_DUPLICATE_EMAIL", email: String(data.email ?? "") });
77
+ }
78
+
79
+ return err({ type: "USER_DATABASE_ERROR", cause });
80
+ }
81
+ }
82
+
83
+ deleteById(id: string): Result<void, UserNotFoundError | UserDatabaseError> {
84
+ try {
85
+ const rows = this.db.delete(users).where(eq(users.id, id)).returning().all();
86
+ if (!rows[0])
87
+ return err({ type: "USER_NOT_FOUND", id });
88
+
89
+ return ok(undefined);
90
+ }
91
+ catch (cause) {
92
+ return err({ type: "USER_DATABASE_ERROR", cause });
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,210 @@
1
+ import type { UserService } from "./user.service";
2
+ import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
3
+ import { match } from "ts-pattern";
4
+ import { toUserResponse } from "./user.handler";
5
+ import {
6
+ CreateUserBodySchema,
7
+ ErrorResponseSchema,
8
+ UpdateUserBodySchema,
9
+ UserIdParamSchema,
10
+ UserResponseSchema,
11
+ } from "./user.schema";
12
+
13
+ const tags = ["Users"];
14
+
15
+ const errorResponses = {
16
+ 404: {
17
+ content: { "application/json": { schema: ErrorResponseSchema } },
18
+ description: "Not found",
19
+ },
20
+ 409: {
21
+ content: { "application/json": { schema: ErrorResponseSchema } },
22
+ description: "Conflict",
23
+ },
24
+ 422: {
25
+ content: { "application/json": { schema: ErrorResponseSchema } },
26
+ description: "Validation error",
27
+ },
28
+ 500: {
29
+ content: { "application/json": { schema: ErrorResponseSchema } },
30
+ description: "Internal server error",
31
+ },
32
+ } as const;
33
+
34
+ const listUsersRoute = createRoute({
35
+ method: "get",
36
+ path: "/",
37
+ tags,
38
+ summary: "List all users",
39
+ responses: {
40
+ 200: {
41
+ content: { "application/json": { schema: z.array(UserResponseSchema) } },
42
+ description: "List of users",
43
+ },
44
+ 500: errorResponses[500],
45
+ },
46
+ });
47
+
48
+ const getUserRoute = createRoute({
49
+ method: "get",
50
+ path: "/{id}",
51
+ tags,
52
+ summary: "Get user by ID",
53
+ request: { params: UserIdParamSchema },
54
+ responses: {
55
+ 200: {
56
+ content: { "application/json": { schema: UserResponseSchema } },
57
+ description: "User found",
58
+ },
59
+ 404: errorResponses[404],
60
+ 500: errorResponses[500],
61
+ },
62
+ });
63
+
64
+ const createUserRoute = createRoute({
65
+ method: "post",
66
+ path: "/",
67
+ tags,
68
+ summary: "Create a new user",
69
+ request: {
70
+ body: {
71
+ content: { "application/json": { schema: CreateUserBodySchema } },
72
+ required: true,
73
+ },
74
+ },
75
+ responses: {
76
+ 201: {
77
+ content: { "application/json": { schema: UserResponseSchema } },
78
+ description: "User created",
79
+ },
80
+ 409: errorResponses[409],
81
+ 422: errorResponses[422],
82
+ 500: errorResponses[500],
83
+ },
84
+ });
85
+
86
+ const updateUserRoute = createRoute({
87
+ method: "patch",
88
+ path: "/{id}",
89
+ tags,
90
+ summary: "Update a user",
91
+ request: {
92
+ params: UserIdParamSchema,
93
+ body: {
94
+ content: { "application/json": { schema: UpdateUserBodySchema } },
95
+ required: true,
96
+ },
97
+ },
98
+ responses: {
99
+ 200: {
100
+ content: { "application/json": { schema: UserResponseSchema } },
101
+ description: "User updated",
102
+ },
103
+ 404: errorResponses[404],
104
+ 409: errorResponses[409],
105
+ 422: errorResponses[422],
106
+ 500: errorResponses[500],
107
+ },
108
+ });
109
+
110
+ const deleteUserRoute = createRoute({
111
+ method: "delete",
112
+ path: "/{id}",
113
+ tags,
114
+ summary: "Delete a user",
115
+ request: { params: UserIdParamSchema },
116
+ responses: {
117
+ 204: { description: "User deleted" },
118
+ 404: errorResponses[404],
119
+ 500: errorResponses[500],
120
+ },
121
+ });
122
+
123
+ export function createUserRouter(service: UserService) {
124
+ const router = new OpenAPIHono({
125
+ defaultHook(result, c) {
126
+ if (!result.success) {
127
+ const message = result.error.issues.map(i => i.message).join(", ");
128
+
129
+ return c.json({ message }, 422);
130
+ }
131
+ },
132
+ });
133
+
134
+ router.openapi(listUsersRoute, (c) => {
135
+ const result = service.listUsers();
136
+ if (result.isErr()) {
137
+ return match(result.error)
138
+ .with({ type: "USER_DATABASE_ERROR" }, () =>
139
+ c.json({ message: "Internal server error" }, 500))
140
+ .exhaustive();
141
+ }
142
+
143
+ return c.json(result.value.map(toUserResponse), 200);
144
+ });
145
+
146
+ router.openapi(getUserRoute, (c) => {
147
+ const { id } = c.req.valid("param");
148
+ const result = service.getUserById(id);
149
+ if (result.isErr()) {
150
+ return match(result.error)
151
+ .with({ type: "USER_NOT_FOUND" }, ({ id: uid }) =>
152
+ c.json({ message: `User ${uid} not found` }, 404))
153
+ .with({ type: "USER_DATABASE_ERROR" }, () =>
154
+ c.json({ message: "Internal server error" }, 500))
155
+ .exhaustive();
156
+ }
157
+
158
+ return c.json(toUserResponse(result.value), 200);
159
+ });
160
+
161
+ router.openapi(createUserRoute, (c) => {
162
+ const body = c.req.valid("json");
163
+ const result = service.createUser(body);
164
+ if (result.isErr()) {
165
+ return match(result.error)
166
+ .with({ type: "USER_DUPLICATE_EMAIL" }, ({ email }) =>
167
+ c.json({ message: `Email ${email} is already taken` }, 409))
168
+ .with({ type: "USER_DATABASE_ERROR" }, () =>
169
+ c.json({ message: "Internal server error" }, 500))
170
+ .exhaustive();
171
+ }
172
+
173
+ return c.json(toUserResponse(result.value), 201);
174
+ });
175
+
176
+ router.openapi(updateUserRoute, (c) => {
177
+ const { id } = c.req.valid("param");
178
+ const body = c.req.valid("json");
179
+ const result = service.updateUser(id, body);
180
+ if (result.isErr()) {
181
+ return match(result.error)
182
+ .with({ type: "USER_NOT_FOUND" }, ({ id: uid }) =>
183
+ c.json({ message: `User ${uid} not found` }, 404))
184
+ .with({ type: "USER_DUPLICATE_EMAIL" }, ({ email }) =>
185
+ c.json({ message: `Email ${email} is already taken` }, 409))
186
+ .with({ type: "USER_DATABASE_ERROR" }, () =>
187
+ c.json({ message: "Internal server error" }, 500))
188
+ .exhaustive();
189
+ }
190
+
191
+ return c.json(toUserResponse(result.value), 200);
192
+ });
193
+
194
+ router.openapi(deleteUserRoute, (c) => {
195
+ const { id } = c.req.valid("param");
196
+ const result = service.deleteUser(id);
197
+ if (result.isErr()) {
198
+ return match(result.error)
199
+ .with({ type: "USER_NOT_FOUND" }, ({ id: uid }) =>
200
+ c.json({ message: `User ${uid} not found` }, 404))
201
+ .with({ type: "USER_DATABASE_ERROR" }, () =>
202
+ c.json({ message: "Internal server error" }, 500))
203
+ .exhaustive();
204
+ }
205
+
206
+ return c.body(null, 204);
207
+ });
208
+
209
+ return router;
210
+ }
@@ -0,0 +1,54 @@
1
+ import { z } from "@hono/zod-openapi";
2
+
3
+ export const UserIdParamSchema = z.object({
4
+ id: z
5
+ .string()
6
+ .uuid()
7
+ .openapi({ description: "User ID (UUID)", example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }),
8
+ });
9
+
10
+ export const CreateUserBodySchema = z.object({
11
+ name: z
12
+ .string()
13
+ .min(1)
14
+ .max(100)
15
+ .openapi({ description: "Display name", example: "Alice" }),
16
+ email: z
17
+ .string()
18
+ .email()
19
+ .openapi({ description: "Email address", example: "alice@example.com" }),
20
+ });
21
+
22
+ export const UpdateUserBodySchema = z.object({
23
+ name: z
24
+ .string()
25
+ .min(1)
26
+ .max(100)
27
+ .optional()
28
+ .openapi({ description: "Display name" }),
29
+ email: z
30
+ .string()
31
+ .email()
32
+ .optional()
33
+ .openapi({ description: "Email address" }),
34
+ });
35
+
36
+ export const UserResponseSchema = z.object({
37
+ id: z
38
+ .string()
39
+ .uuid()
40
+ .openapi({ example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }),
41
+ name: z.string().openapi({ example: "Alice" }),
42
+ email: z.string().openapi({ example: "alice@example.com" }),
43
+ createdAt: z
44
+ .string()
45
+ .datetime()
46
+ .openapi({ example: "2024-01-01T00:00:00.000Z" }),
47
+ });
48
+
49
+ export const ErrorResponseSchema = z.object({
50
+ message: z.string().openapi({ example: "User not found" }),
51
+ });
52
+
53
+ export type CreateUserBody = z.infer<typeof CreateUserBodySchema>;
54
+ export type UpdateUserBody = z.infer<typeof UpdateUserBodySchema>;
@@ -0,0 +1,35 @@
1
+ import type { Result } from "neverthrow";
2
+ import type {
3
+ UserDatabaseError,
4
+ UserDuplicateEmailError,
5
+ UserNotFoundError,
6
+ } from "./user.errors";
7
+ import type { UserRepository, UserRow } from "./user.repo";
8
+ import type { CreateUserBody, UpdateUserBody } from "./user.schema";
9
+
10
+ export class UserService {
11
+ constructor(private readonly userRepo: UserRepository) {}
12
+
13
+ listUsers(): Result<UserRow[], UserDatabaseError> {
14
+ return this.userRepo.findAll();
15
+ }
16
+
17
+ getUserById(id: string): Result<UserRow, UserNotFoundError | UserDatabaseError> {
18
+ return this.userRepo.findById(id);
19
+ }
20
+
21
+ createUser(body: CreateUserBody): Result<UserRow, UserDuplicateEmailError | UserDatabaseError> {
22
+ return this.userRepo.create(body);
23
+ }
24
+
25
+ updateUser(
26
+ id: string,
27
+ body: UpdateUserBody,
28
+ ): Result<UserRow, UserNotFoundError | UserDuplicateEmailError | UserDatabaseError> {
29
+ return this.userRepo.update(id, body);
30
+ }
31
+
32
+ deleteUser(id: string): Result<void, UserNotFoundError | UserDatabaseError> {
33
+ return this.userRepo.deleteById(id);
34
+ }
35
+ }
@@ -0,0 +1,59 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import process from "node:process";
3
+ import { createAdaptorServer } from "@hono/node-server";
4
+ import pinoHttp from "pino-http";
5
+ import { createApp } from "./app";
6
+ import { env } from "./config/env";
7
+ import { logger } from "./lib/logger";
8
+
9
+ const app = createApp();
10
+ const httpLogger = pinoHttp({ logger });
11
+
12
+ const server = createAdaptorServer(app);
13
+
14
+ const listeners = server.listeners("request") as Array<
15
+ (req: IncomingMessage, res: ServerResponse) => void
16
+ >;
17
+ const requestListener = listeners[0];
18
+ if (!requestListener)
19
+ throw new Error("No request listener found on server");
20
+
21
+ server.removeAllListeners("request");
22
+ server.on("request", (req: IncomingMessage, res: ServerResponse) => {
23
+ httpLogger(req, res, () => requestListener(req, res));
24
+ });
25
+
26
+ server.on("error", (err: NodeJS.ErrnoException) => {
27
+ if (err.code === "EADDRINUSE") {
28
+ logger.warn(`Port ${env.PORT} in use, retrying in 1s...`);
29
+ setTimeout(() => server.listen(env.PORT), 1000);
30
+ }
31
+ else {
32
+ logger.error(err);
33
+ process.exit(1);
34
+ }
35
+ });
36
+
37
+ server.listen(env.PORT, () => {
38
+ logger.info(`Server running at http://localhost:${env.PORT}`);
39
+ if (env.NODE_ENV !== "production") {
40
+ logger.info(`API docs: http://localhost:${env.PORT}/docs`);
41
+ logger.info(`OpenAPI: http://localhost:${env.PORT}/api/openapi.json`);
42
+ }
43
+ });
44
+
45
+ process.on("SIGINT", () => {
46
+ server.close();
47
+ process.exit(0);
48
+ });
49
+
50
+ process.on("SIGTERM", () => {
51
+ setTimeout(() => process.exit(0), 1000).unref();
52
+ server.close((err) => {
53
+ if (err) {
54
+ logger.error(err);
55
+ process.exit(1);
56
+ }
57
+ process.exit(0);
58
+ });
59
+ });
@@ -0,0 +1,16 @@
1
+ import pino from "pino";
2
+ import { env } from "../config/env";
3
+
4
+ export const logger = pino({
5
+ level: env.LOG_LEVEL,
6
+ ...(env.NODE_ENV === "development" && {
7
+ transport: {
8
+ target: "pino-pretty",
9
+ options: {
10
+ colorize: true,
11
+ translateTime: "SYS:standard",
12
+ ignore: "pid,hostname",
13
+ },
14
+ },
15
+ }),
16
+ });
@@ -0,0 +1,8 @@
1
+ import type { ErrorHandler } from "hono";
2
+ import { logger } from "../lib/logger";
3
+
4
+ export const errorHandler: ErrorHandler = (err, c) => {
5
+ logger.error({ err }, "Unhandled error");
6
+
7
+ return c.json({ message: "Internal server error" }, 500);
8
+ };
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "paths": {
7
+ "@/*": ["./src/*"]
8
+ },
9
+ "types": ["node"],
10
+ "strict": true,
11
+ "noUncheckedIndexedAccess": true,
12
+ "skipLibCheck": true
13
+ },
14
+ "include": ["src", "drizzle.config.ts", "vitest.config.ts"]
15
+ }
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: "node",
7
+ coverage: {
8
+ provider: "v8",
9
+ reporter: ["text", "html"],
10
+ },
11
+ },
12
+ resolve: {
13
+ alias: {
14
+ "@": new URL("./src", import.meta.url).pathname,
15
+ },
16
+ },
17
+ });
@@ -1,4 +1,4 @@
1
1
  node_modules
2
2
  dist
3
3
 
4
- .DS_store
4
+ .DS_Store
@@ -32,5 +32,9 @@
32
32
  "scss",
33
33
  "pcss",
34
34
  "postcss"
35
- ]
35
+ ],
36
+
37
+ // Use workspace TypeScript so that editor diagnostics are consistent with the project compiler.
38
+ // If prompted, run "TypeScript: Select TypeScript Version" and choose "Use Workspace Version".
39
+ "js/ts.tsdk.path": "node_modules/typescript/lib"
36
40
  }
@@ -13,7 +13,13 @@ export default antfu(
13
13
  "style/jsx-first-prop-new-line": ["error", "never"],
14
14
  "style/jsx-max-props-per-line": "off",
15
15
  "style/jsx-closing-bracket-location": ["error", "after-props"],
16
- "style/operator-linebreak": ["error", "before"],
16
+ "style/operator-linebreak": ["error", "before", { overrides: { "=": "after" } }],
17
+ "style/padding-line-between-statements": [
18
+ "error",
19
+ { blankLine: "always", prev: "*", next: "for" },
20
+ { blankLine: "always", prev: "for", next: "*" },
21
+ { blankLine: "always", prev: "*", next: "return" },
22
+ ],
17
23
  },
18
24
  },
19
25
  },
@@ -3,8 +3,11 @@
3
3
  "paths": {
4
4
  "@/*": ["./src/*"]
5
5
  },
6
+ "strict": true,
7
+ "noUncheckedIndexedAccess": true,
6
8
  "declaration": true,
7
- "isolatedDeclarations": true
9
+ "isolatedDeclarations": true,
10
+ "skipLibCheck": true
8
11
  },
9
12
  "include": ["src"]
10
13
  }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ resolve: {
5
+ alias: {
6
+ "@": new URL("./src", import.meta.url).pathname,
7
+ },
8
+ },
9
+ test: {
10
+ environment: "node",
11
+ },
12
+ });
@@ -1,4 +1,4 @@
1
1
  node_modules
2
2
  dist
3
3
 
4
- .DS_store
4
+ .DS_Store
@@ -32,5 +32,9 @@
32
32
  "scss",
33
33
  "pcss",
34
34
  "postcss"
35
- ]
35
+ ],
36
+
37
+ // Use workspace TypeScript so that editor diagnostics are consistent with the project compiler.
38
+ // If prompted, run "TypeScript: Select TypeScript Version" and choose "Use Workspace Version".
39
+ "js/ts.tsdk.path": "node_modules/typescript/lib"
36
40
  }
@@ -13,7 +13,7 @@ export default antfu(
13
13
  "style/jsx-first-prop-new-line": ["error", "never"],
14
14
  "style/jsx-max-props-per-line": "off",
15
15
  "style/jsx-closing-bracket-location": ["error", "after-props"],
16
- "style/operator-linebreak": ["error", "before"],
16
+ "style/operator-linebreak": ["error", "before", { overrides: { "=": "after" } }],
17
17
  "style/padding-line-between-statements": [
18
18
  "error",
19
19
  { blankLine: "always", prev: "*", next: "for" },
@@ -3,7 +3,10 @@
3
3
  "paths": {
4
4
  "@/*": ["./src/*"]
5
5
  },
6
- "types": ["node"]
6
+ "types": ["node"],
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "skipLibCheck": true
7
10
  },
8
11
  "include": ["src"]
9
12
  }