express-generator-typescript 2.9.1 → 3.0.2

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 (36) hide show
  1. package/README.md +46 -40
  2. package/lib/template/README.md +33 -15
  3. package/lib/template/package.json +11 -21
  4. package/lib/template/src/common/{utils → classes}/route-errors.ts +1 -1
  5. package/lib/template/src/common/constants/HttpStatusCodes.ts +1 -1
  6. package/lib/template/src/common/constants/env-inv.ts +2 -1
  7. package/lib/template/src/common/utils/dev-only.ts +25 -0
  8. package/lib/template/src/entities/User/_internal/validators.ts +47 -0
  9. package/lib/template/src/entities/User/index.ts +2 -0
  10. package/lib/template/src/entities/User/module.ts +63 -0
  11. package/lib/template/src/entities/User/types.ts +18 -0
  12. package/lib/template/src/{models → entities}/common/types.ts +1 -1
  13. package/lib/template/src/public/scripts/HttpClient.js +59 -0
  14. package/lib/template/src/public/scripts/renderUsers.js +43 -5
  15. package/lib/template/src/public/scripts/users.js +75 -14
  16. package/lib/template/src/repos/MockOrm.ts +3 -3
  17. package/lib/template/src/repos/UserRepo.ts +6 -6
  18. package/lib/template/src/repos/common/database.json +2 -2
  19. package/lib/template/src/routes/UserRoutes.ts +2 -2
  20. package/lib/template/src/routes/common/express-types.ts +1 -1
  21. package/lib/template/src/routes/common/parseReq.ts +2 -2
  22. package/lib/template/src/server.ts +16 -4
  23. package/lib/template/src/services/UserService.ts +6 -6
  24. package/lib/template/src/views/users.html +1 -1
  25. package/lib/template/tests/common/comparators.ts +3 -3
  26. package/lib/template/tests/common/supertest-types.ts +2 -2
  27. package/lib/template/tests/frontend.test.ts +1 -1
  28. package/lib/template/tests/support/agent.ts +2 -2
  29. package/lib/template/tests/users.test.ts +11 -11
  30. package/lib/template/tsconfig.json +9 -10
  31. package/lib/template/tsconfig.prod.json +13 -4
  32. package/lib/template/{vitest.config.mts → vitest.config.ts} +6 -4
  33. package/package.json +1 -1
  34. package/lib/template/bs-config.js +0 -8
  35. package/lib/template/src/models/User.model.ts +0 -95
  36. package/lib/template/src/public/scripts/http.js +0 -25
package/README.md CHANGED
@@ -8,23 +8,23 @@
8
8
  [![npm downloads](https://img.shields.io/npm/dm/express-generator-typescript?color=orange)](https://www.npmjs.com/package/express-generator-typescript)
9
9
  [![License](https://img.shields.io/npm/l/express-generator-typescript)](https://github.com/seanpmaxwell/express-generator-typescript/blob/main/LICENSE)
10
10
 
11
- Command line tool which generates production-ready express templates with TypeScript baked in. Spin up a web server in seconds that follows the [TypeScript best practices](https://github.com/seanpmaxwell/Typescript-Best-Practices/blob/main/README.md).
11
+ A command-line tool that generates production-ready Express projects with TypeScript built in. Spin up a web server in seconds that follows the [TypeScript best practices](https://github.com/seanpmaxwell/Typescript-Best-Practices).
12
12
 
13
13
  <p align="center">· · ·</p>
14
14
 
15
15
  ## 🧭 Overview
16
16
 
17
- `express-generator-typescript` creates a new Express application similar to the classic `express-generator` package, but the generated project is fully wired for TypeScript. You get strict typing, linting, hot reloading, production builds, testing utilities, and sane defaults that focus on APIs (no view engine or opinionated ORM). Path aliases are preconfigured through `tsconfig-paths` and `_moduleAliases`, so referencing modules stays clean even as the app grows.
17
+ `express-generator-typescript` works like the classic `express-generator` package, but the project it creates is fully set up for TypeScript. You get strict typing, linting, hot reloading, testing, and production builds, with defaults aimed at APIs. The project is an ES module and comes with an `@src/*` import alias, so imports stay clean as the app grows.
18
18
 
19
19
  <p align="center">· · ·</p>
20
20
 
21
21
  ## ✨ Features
22
22
 
23
- - **TypeScript-first** – strict compiler settings, linting, and sensible tsconfig defaults ready to go.
24
- - **API-centric** – no view engine or extra dependencies; ideal for SPAs, mobile backends, or services.
25
- - **Productivity tooling** – includes nodemon, ts-node, hot reload scripts, Vitest, ESLint, and production builds.
26
- - **Path aliases** – `@src/*` aliases configured in `tsconfig.json` (and `_moduleAliases` in `package.json` for production) so you can import modules cleanly.
27
- - **Keeps dependencies lean** – no bundled ORM or UI layers; only the essentials for Express + TS development.
23
+ - **TypeScript-first** – strict compiler settings, linting, and sensible tsconfig defaults, ready to go.
24
+ - **Built for APIs** – ideal for SPAs, mobile backends, or services.
25
+ - **Fast development** – runs TypeScript directly with tsx (no build step), restarts the server when you change it, and refreshes the browser when you change front-end files. Vitest, ESLint, and production builds are included.
26
+ - **Path aliases** – import from `@src/*` anywhere. It works in development, tests, and production builds.
27
+ - **Lean dependencies** – no view engine, ORM, or UI layer; only the essentials for Express + TypeScript.
28
28
 
29
29
  <p align="center">· · ·</p>
30
30
 
@@ -52,65 +52,71 @@ cd my-api
52
52
  npm run dev
53
53
  ```
54
54
 
55
- Use `--use-yarn` if you prefer Yarn over npm. If you omit the project name, the generator creates `express-gen-ts`.
56
-
57
55
  <p align="center">· · ·</p>
58
56
 
59
57
  ## 🖥️ CLI Options
60
58
 
61
- | Option | Description |
62
- | ----------------- | --------------------------------------------------------------------------- |
63
- | `project name` | Folder to create. Defaults to `express-gen-ts` if omitted. |
64
- | `--use-yarn` | Installs dependencies with Yarn instead of npm. |
65
- | `--force` | Writes into the target folder even if it is not empty (existing files with the same names are overwritten). |
66
- | `-h`, `--help` | Shows usage. |
67
- | `-v`, `--version` | Shows the generator version. |
59
+ | Option | Description |
60
+ | ----------------- | ------------------------------------------------------------------ |
61
+ | `project name` | Folder to create. Defaults to `express-gen-ts`. |
62
+ | `--use-yarn` | Install dependencies with Yarn instead of npm. |
63
+ | `--force` | Write into a folder that isn't empty. Files with the same name are overwritten. |
64
+ | `-h`, `--help` | Show usage. |
65
+ | `-v`, `--version` | Show the generator version. |
68
66
 
69
- > The generator refuses to write into a folder that already has files in it, so it can't overwrite your work by accident.
67
+ > Without `--force`, the generator won't write into a folder that already has files in it, so it can't overwrite your work by accident.
70
68
 
71
69
  <p align="center">· · ·</p>
72
70
 
73
71
  ## 🧩 Generated Template
74
72
 
75
- The generated template is a CRUD app for the `User` record to demonstrate model, services, and routing patterns in Express + TypeScript. Commands for linting, transpiling, formatting, and hot-reloading are all configured for you.
73
+ The generated project is a small CRUD app for a `User` record. It shows how to structure models, services, and routes in Express + TypeScript. Linting, formatting, building, and hot reloading are all set up for you.
76
74
 
77
75
  ### Available `package.json` Scripts
78
76
 
79
- - `npm run dev` – Run the server in dev mode with live reload and browser refresh.
80
- - `npm run test` - Run tests with vitest.
81
- - `npm run test -- users.test.ts` – Target a single test file.
82
- - `npm run lint` – Run ESLint checks.
83
- - `npm run format` - Run prettier.
84
- - `npm run build` – Compile the project for production.
85
- - `npm start` – Serve the built project.
86
- - `npm run typecheck` – Run the TypeScript compiler without emitting files.
77
+ - `npm run dev` – Run the server in development with live reload and browser refresh.
78
+ - `npm test` – Run the tests with Vitest.
79
+ - `npm test -- users.test.ts` – Run a single test file.
80
+ - `npm run lint` – Check the code with ESLint.
81
+ - `npm run format` – Format the code with Prettier.
82
+ - `npm run build` – Build the project for production.
83
+ - `npm start` – Run the production build.
84
+ - `npm run typecheck` – Check for TypeScript errors without building.
87
85
  - `npm run install:clean` – Delete `node_modules` and the lockfile, then reinstall.
88
86
 
89
87
  ### Architecture
90
88
 
91
- Because this is a small CRUD app, **layered** is the architectural pattern of choice. However, you should consider switching to a **domain-based** layout if you plan on scaling. There is a good tutorial [here](https://github.com/seanpmaxwell/Typescript-Best-Practices/tree/main?tab=readme-ov-file#architecture) in the _Typescript Best Practices README_ about architectural patterns with TypeScript.
89
+ The app uses a **layered** architecture, which suits a small CRUD app. If you plan to grow it, consider switching to a **domain-based** layout. The [Typescript Best Practices README](https://github.com/seanpmaxwell/Typescript-Best-Practices/tree/main?tab=readme-ov-file#architecture) explains both patterns.
92
90
 
93
91
  Layers explained:
94
- ```yml
95
- - src/ <-- source code
96
- - common/
92
+ ```md
93
+ - config/ → .env files for each environment
94
+ - src/ → Source code
95
+ - common/ → Shared code
96
+ - classes/ → Shared classes (e.g. route errors)
97
97
  - constants/
98
- - Paths.ts <-- Single source of truth for all API routes
99
- - routes/ <-- extracting and validating values from express Request/Response objects
100
- - services/ <-- Business logic (where everything comes together)
101
- - repos/ <-- Talking to the database layer
102
- - models/ <-- For describing/handling objects representing database records
103
- - tests/ <-- unit-tests
98
+ - Paths.ts → Single source of truth for all API routes
99
+ - types/ → Shared types
100
+ - utils/ → Shared helper functions
101
+ - entities/ → Describe and handle database records (one folder per entity)
102
+ - routes/ → Read and validate values from Express requests; send responses
103
+ - services/ → Business logic (where everything comes together)
104
+ - repos/ → Talk to the database
105
+ - public/ → Front-end scripts and styles
106
+ - views/ → HTML pages
107
+ - main.ts → Starts the server
108
+ - server.ts → Sets up the Express app
109
+ - tests/ → Tests
104
110
  ```
105
111
 
106
112
  <p align="center">· · ·</p>
107
113
 
108
- ## Notes for VSCode users
114
+ ## Notes for VS Code users
109
115
 
110
116
  <details>
111
117
  <summary>Format on save</summary>
112
118
 
113
- The generated template uses `eslint`+`prettier`, so if you want features like _formatting on save_, you need to make sure to install the prettier extension for VSCode and set it as your default formatter in `.vscode/setting.json`:
119
+ The generated project uses ESLint for linting and Prettier for formatting. To format on save, install the Prettier extension for VS Code and set it as the default formatter in `.vscode/settings.json`:
114
120
 
115
121
  ```json
116
122
  // .vscode/settings.json
@@ -160,7 +166,7 @@ The generated template uses `eslint`+`prettier`, so if you want features like _f
160
166
  <details>
161
167
  <summary>Debugging</summary>
162
168
 
163
- If you want to debug in VSCode with breakpoints you need to start the processes through `.vscode/launch.json`:
169
+ To debug with breakpoints in VS Code, start the app or tests from `.vscode/launch.json`:
164
170
 
165
171
  ```json
166
172
  // .vscode/launch.json
@@ -168,7 +174,7 @@ If you want to debug in VSCode with breakpoints you need to start the processes
168
174
  "version": "0.2.0",
169
175
  "configurations": [
170
176
  {
171
- "name": "Dev - ts-node",
177
+ "name": "Dev",
172
178
  "type": "node",
173
179
  "request": "launch",
174
180
  "runtimeExecutable": "npm",
@@ -2,32 +2,29 @@
2
2
 
3
3
  This project was created with [express-generator-typescript](https://github.com/seanpmaxwell/express-generator-typescript). It requires Node.js 22.12 or newer.
4
4
 
5
- <p align="center">· · ·</p>
5
+ The original template follows the [TypeScript best practices](https://github.com/seanpmaxwell/Typescript-Best-Practices).
6
6
 
7
+ <p align="center">· · ·</p>
7
8
 
8
9
  ## Available Scripts
9
10
 
10
- ### `npm run install:clean`
11
-
12
- Remove the existing `node_modules/` folder, `package-lock.json`, and reinstall all library modules.
13
-
14
- ### `npm run dev`
11
+ ### `npm run dev`
15
12
 
16
- Run the server in development with hot reloading and browser refresh (see `package.json` for all `npm run dev` variations)<br/>
13
+ Run the server in development at http://localhost:3000. The server restarts when you change server code, and the browser refreshes when you change server code or anything in `src/public` or `src/views`.
17
14
 
18
- **IMPORTANT** development mode uses `swc` for performance reasons which DOES NOT check for typescript errors. Run `npm run typecheck` to check for type errors. NOTE: you should use your IDE to prevent most type errors.
15
+ > **Note:** development mode runs your `.ts` files directly with `tsx`, which doesn't check for TypeScript errors. Run `npm run typecheck` to check for them, and let your editor catch most of them as you work.
19
16
 
20
17
  ### `npm test`
21
18
 
22
- Run unit-tests with <a href="https://vitest.dev/guide/">vitest</a>.
19
+ Run the tests with [Vitest](https://vitest.dev/guide/).
23
20
 
24
21
  ### `npm run lint`
25
22
 
26
- Check for linting errors.
23
+ Check the code with ESLint.
27
24
 
28
25
  ### `npm run format`
29
26
 
30
- Format `src/` and `tests/` with prettier.
27
+ Format `src/` and `tests/` with Prettier.
31
28
 
32
29
  ### `npm run build`
33
30
 
@@ -35,16 +32,37 @@ Build the project for production.
35
32
 
36
33
  ### `npm start`
37
34
 
38
- Run the production build (Must be built first).
35
+ Run the production build. Run `npm run build` first.
39
36
 
40
37
  ### `npm run typecheck`
41
38
 
42
- Check for typescript errors.
39
+ Check for TypeScript errors without building.
40
+
41
+ ### `npm run install:clean`
42
+
43
+ Delete `node_modules/` and `package-lock.json`, then reinstall all dependencies.
43
44
 
44
45
  <p align="center">· · ·</p>
45
46
 
47
+ ## Tech Stack
48
+
49
+ - **Language**: [TypeScript](https://www.typescriptlang.org/) (strict mode, ES modules)
50
+ - **Web server framework**: [Express](https://expressjs.com/en/) (v5)
51
+ - **Security headers**: [helmet](https://helmet.js.org/) (production only)
52
+ - **Logging**
53
+ - **Request logging**: [morgan](https://github.com/expressjs/morgan) (development only)
54
+ - **General logging**: [jet-logger](https://github.com/seanpmaxwell/jet-logger)
55
+ - **Validation**: [jet-validators](https://github.com/seanpmaxwell/jet-validators)
56
+ - **Environment variables**: [dotenv](https://github.com/motdotla/dotenv) loads `config/.env.*`, and [jet-env](https://github.com/seanpmaxwell/jet-env) validates them
57
+ - **Reloading**: [tsx](https://tsx.hirok.io) (`tsx watch` restarts the server) and [livereload](https://github.com/napcs/node-livereload) + [connect-livereload](https://github.com/intesso/connect-livereload) (refreshes the browser)
58
+ - **Testing**: [Vitest](https://vitest.dev) + [Supertest](https://github.com/ladjs/supertest)
59
+ - **Linting**: [ESLint](https://eslint.org) with [typescript-eslint](https://typescript-eslint.io/packages/typescript-eslint) and [eslint-plugin-n](https://github.com/eslint-community/eslint-plugin-n)
60
+ - **Formatting**: [Prettier](https://prettier.io) with [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports)
61
+ - **Building**: `tsc` + [tsc-alias](https://github.com/justkey007/tsc-alias) (rewrites `@src/*` imports in `dist/`)
62
+
63
+ <p align="center">· · ·</p>
46
64
 
47
65
  ## Additional Notes
48
66
 
49
- - `config/.env.production` is listed in `.gitignore` so production secrets don't get committed. Keep it out of version control and provide its values through your deployment tooling.
50
- - The database is a JSON file (`src/repos/common/database.json`, or `dist/repos/common/database.json` in production) meant only for the demo. It's created automatically if missing and isn't safe for concurrent writes, so replace `src/repos/MockOrm.ts` with a real database before going to production.
67
+ - `config/.env.production` is in `.gitignore` so production secrets don't get committed. Keep it out of version control and supply its values through your deployment tooling.
68
+ - The database is a JSON file meant only for the demo: `src/repos/common/database.json` in development, or `dist/repos/common/database.json` in production. It's created automatically if missing, but it isn't safe for simultaneous writes. Replace `src/repos/MockOrm.ts` with a real database before going to production.
@@ -1,26 +1,21 @@
1
1
  {
2
2
  "name": "express-typescript-example",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
+ "type": "module",
4
5
  "scripts": {
5
- "build": "shx rm -rf dist && npm run lint && tsc --project tsconfig.prod.json && npm run build:copy-static",
6
+ "build": "shx rm -rf dist && npm run lint && tsc --project tsconfig.prod.json && tsc-alias -p tsconfig.prod.json && npm run build:copy-static",
6
7
  "build:copy-static": "shx mkdir -p dist/public dist/views && shx cp -r src/public/* dist/public && shx cp -r src/views/* dist/views",
7
8
  "install:clean": "shx rm -rf ./node_modules package-lock.json && npm i",
8
- "dev:basic": "cross-env DOTENV_CONFIG_PATH=./config/.env.development ts-node ./src/main",
9
- "dev:watch": "nodemon --exec \"npm run dev:basic\" --watch ./src --ext .ts",
10
- "dev": "concurrently \"npm run dev:watch\" \"npm run sync\"",
9
+ "dev": "cross-env DOTENV_CONFIG_PATH=./config/.env.development tsx watch --import dotenv/config ./src/main.ts",
11
10
  "lint": "eslint .",
12
11
  "format": "prettier --write .",
13
- "start": "cross-env DOTENV_CONFIG_PATH=./config/.env.production node -r dotenv/config -r module-alias/register ./dist/main.js",
14
- "sync": "delay 1s && browser-sync start --config bs-config.js",
12
+ "start": "cross-env DOTENV_CONFIG_PATH=./config/.env.production node --import dotenv/config ./dist/main.js",
15
13
  "test": "cross-env NODE_ENV=test vitest",
16
14
  "typecheck": "tsc -b --noEmit"
17
15
  },
18
16
  "engines": {
19
17
  "node": ">=22.12.0"
20
18
  },
21
- "_moduleAliases": {
22
- "@src": "dist"
23
- },
24
19
  "dependencies": {
25
20
  "cross-env": "^10.1.0",
26
21
  "dotenv": "^18.0.4",
@@ -32,36 +27,31 @@
32
27
  "jet-paths": "^4.0.2",
33
28
  "jet-validators": "^2.3.1",
34
29
  "jsonfile": "^6.2.1",
35
- "module-alias": "^2.3.4",
36
30
  "morgan": "^1.12.1"
37
31
  },
38
32
  "devDependencies": {
39
33
  "@eslint/js": "^10.0.1",
40
- "@swc/core": "^1.16.2",
41
34
  "@trivago/prettier-plugin-sort-imports": "^6.0.2",
35
+ "@types/connect-livereload": "^0.6.3",
42
36
  "@types/express": "^5.0.6",
43
37
  "@types/jsonfile": "^6.1.4",
38
+ "@types/livereload": "^0.9.5",
44
39
  "@types/morgan": "^1.9.10",
45
40
  "@types/node": "^26.6.2",
46
41
  "@types/supertest": "^7.2.1",
47
- "browser-sync": "^3.0.4",
48
- "concurrently": "^10.0.5",
49
- "delay-cli": "^3.0.0",
42
+ "connect-livereload": "^0.6.1",
50
43
  "eslint": "^10.11.0",
51
44
  "eslint-config-prettier": "^10.1.8",
52
45
  "eslint-plugin-n": "^18.3.0",
53
46
  "jiti": "^2.7.0",
54
- "nodemon": "^3.1.14",
47
+ "livereload": "^0.10.3",
55
48
  "prettier": "^3.9.9",
56
49
  "shx": "^0.4.0",
57
50
  "supertest": "^7.3.0",
58
- "ts-node": "^10.9.2",
59
- "tsconfig-paths": "^4.2.0",
51
+ "tsc-alias": "^1.9.5",
52
+ "tsx": "^4.23.15",
60
53
  "typescript": "^6.0.3",
61
54
  "typescript-eslint": "^8.70.1",
62
55
  "vitest": "^5.0.2"
63
- },
64
- "allowScripts": {
65
- "@swc/core@1.16.2": true
66
56
  }
67
57
  }
@@ -1,4 +1,4 @@
1
- import { ParseError } from 'jet-validators/utils';
1
+ import type { ParseError } from 'jet-validators/utils';
2
2
 
3
3
  import HttpStatusCodes from '@src/common/constants/HttpStatusCodes';
4
4
 
@@ -1,4 +1,4 @@
1
- import { ValueOf } from '../types/utility-types';
1
+ import type { ValueOf } from '../types/utility-types';
2
2
 
3
3
  // ========================================================================= //
4
4
  // CONSTANTS //
@@ -1,5 +1,6 @@
1
1
  import jetEnv, { num } from 'jet-env';
2
- import { ValueOf } from '../types/utility-types';
2
+
3
+ import type { ValueOf } from '../types/utility-types';
3
4
 
4
5
  // ========================================================================= //
5
6
  // CONSTANTS //
@@ -0,0 +1,25 @@
1
+ import connectLiveReload from 'connect-livereload';
2
+ import type { Express } from 'express';
3
+ import livereload from 'livereload';
4
+
5
+ // ========================================================================= //
6
+ // FUNCTIONS //
7
+ // ========================================================================= //
8
+
9
+ /**
10
+ * Development only: refresh the browser when front-end files change, and
11
+ * after `tsx watch` restarts the server for a TypeScript change.
12
+ *
13
+ * Must be registered before the routes that serve html so the reload script
14
+ * gets injected into those pages.
15
+ */
16
+ export function setupLiveReload(app: Express, watchDirs: string[]): void {
17
+ const server = livereload.createServer();
18
+ server.watch(watchDirs);
19
+ // A new process means the backend just restarted; reload once the browser
20
+ // reconnects so it picks up the new server code.
21
+ server.server.once('connection', () => {
22
+ setTimeout(() => server.refresh('/'), 100);
23
+ });
24
+ app.use(connectLiveReload());
25
+ }
@@ -0,0 +1,47 @@
1
+ import jetid from 'jet-id';
2
+ import { isNonEmptyString, isString } from 'jet-validators';
3
+ import { parseObject, type Schema, testObject } from 'jet-validators/utils';
4
+
5
+ import { isISOString } from '@src/common/utils/date-utils';
6
+
7
+ import type { UserEntity, UserInput } from '../types';
8
+
9
+ // ========================================================================= //
10
+ // FUNCTIONS //
11
+ // ========================================================================= //
12
+
13
+ const schema: Schema<UserEntity> = {
14
+ id: isUserId,
15
+ name: isString,
16
+ email: isString,
17
+ created: isISOString,
18
+ };
19
+
20
+ /**
21
+ * Validate the `User` schema.
22
+ */
23
+ export const parseUser = parseObject<UserEntity>(schema);
24
+
25
+ /**
26
+ * For the APIs make sure the right fields are complete
27
+ */
28
+ export const isCompleteUser = testObject<UserEntity>({
29
+ ...schema,
30
+ name: isNonEmptyString,
31
+ email: isNonEmptyString,
32
+ });
33
+
34
+ /**
35
+ * Validate the fields a client sends to create a user.
36
+ */
37
+ export const isUserInput = testObject<UserInput>({
38
+ name: isNonEmptyString,
39
+ email: isNonEmptyString,
40
+ });
41
+
42
+ /**
43
+ * Test if an id is a valid user id.
44
+ */
45
+ export function isUserId(val: unknown): val is string {
46
+ return jetid.test(val);
47
+ }
@@ -0,0 +1,2 @@
1
+ export { default as default } from './module';
2
+ export type { UserEntity, UserInput } from './types';
@@ -0,0 +1,63 @@
1
+ import jetid from 'jet-id';
2
+
3
+ import { getISOString, type ISOString } from '@src/common/utils/date-utils';
4
+
5
+ import {
6
+ isCompleteUser,
7
+ isUserId,
8
+ isUserInput,
9
+ parseUser,
10
+ } from './_internal/validators';
11
+ import type { UserEntity } from './types';
12
+
13
+ // ========================================================================= //
14
+ // FUNCTIONS //
15
+ // ========================================================================= //
16
+
17
+ /**
18
+ * Get a new `UserEntity` object with default values.
19
+ */
20
+ function getDefaults(): UserEntity {
21
+ return {
22
+ id: jetid(),
23
+ name: '',
24
+ email: '',
25
+ created: getISOString(),
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Factory-function.
31
+ *
32
+ * Create a `UserEntity` from a partial or `undefined`
33
+ */
34
+ function create(user?: Partial<UserEntity>): UserEntity {
35
+ return parseUser({ ...getDefaults(), ...user }, (errors) => {
36
+ throw new Error('Setup new user failed ' + JSON.stringify(errors, null, 2));
37
+ });
38
+ }
39
+
40
+ /**
41
+ * Factory-function.
42
+ *
43
+ * Create a `UserEntity` from individual properties
44
+ */
45
+ function of(name: string, email?: string, created?: ISOString): UserEntity {
46
+ const retVal = getDefaults();
47
+ if (name) retVal.name = name;
48
+ if (email) retVal.email = email;
49
+ if (created) retVal.created = created;
50
+ return retVal;
51
+ }
52
+
53
+ // ========================================================================= //
54
+ // EXPORT //
55
+ // ========================================================================= //
56
+
57
+ export default {
58
+ of,
59
+ create,
60
+ isId: isUserId,
61
+ isComplete: isCompleteUser,
62
+ isInput: isUserInput,
63
+ } as const;
@@ -0,0 +1,18 @@
1
+ import type { Entity } from '@src/entities/common/types';
2
+
3
+ // ========================================================================= //
4
+ // TYPES //
5
+ // ========================================================================= //
6
+
7
+ /**
8
+ * @entity `users`
9
+ */
10
+ export interface UserEntity extends Entity {
11
+ name: string;
12
+ email: string;
13
+ }
14
+
15
+ /**
16
+ * Fields a client supplies when creating a user; the server sets the rest.
17
+ */
18
+ export type UserInput = Pick<UserEntity, 'name' | 'email'>;
@@ -1,4 +1,4 @@
1
- import { ISOString } from "@src/common/utils/date-utils";
1
+ import type { ISOString } from '@src/common/utils/date-utils';
2
2
 
3
3
  // ========================================================================= //
4
4
  // TYPES //
@@ -0,0 +1,59 @@
1
+ // ========================================================================= //
2
+ // TYPES //
3
+ // ========================================================================= //
4
+
5
+ /**
6
+ * Thin wrapper around `fetch` for calling the JSON API. Each method sends and
7
+ * accepts JSON and resolves with the raw `Response`.
8
+ *
9
+ * @typedef {object} HttpClient
10
+ * @property {(path: string) => Promise<Response>} get
11
+ * @property {(path: string, data: unknown) => Promise<Response>} post - `data`
12
+ * is sent as the JSON request body.
13
+ * @property {(path: string, data: unknown) => Promise<Response>} put - `data`
14
+ * is sent as the JSON request body.
15
+ * @property {(path: string) => Promise<Response>} delete
16
+ */
17
+
18
+ // ========================================================================= //
19
+ // EXEC //
20
+ // ========================================================================= //
21
+
22
+ /** @type {HttpClient} */
23
+ var HttpClient = (() => {
24
+ /**
25
+ * Build `fetch` options for a JSON request.
26
+ *
27
+ * Used by:
28
+ * {@link HttpClient.get}
29
+ * {@link HttpClient.post}
30
+ * {@link HttpClient.put}
31
+ * {@link HttpClient.delete}
32
+ *
33
+ * @param {string} verb - HTTP method, e.g. "GET".
34
+ * @param {unknown} [data] - Request body; serialized to JSON when present.
35
+ * @returns {RequestInit}
36
+ */
37
+ var getOptions = (verb, data) => {
38
+ var options = {
39
+ dataType: 'json',
40
+ method: verb,
41
+ headers: {
42
+ Accept: 'application/json',
43
+ 'Content-Type': 'application/json',
44
+ },
45
+ };
46
+ if (!!data) {
47
+ options.body = JSON.stringify(data);
48
+ }
49
+ return options;
50
+ };
51
+
52
+ // Set HttpClient methods
53
+ return {
54
+ get: (path) => fetch(path, getOptions('GET')),
55
+ post: (path, data) => fetch(path, getOptions('POST', data)),
56
+ put: (path, data) => fetch(path, getOptions('PUT', data)),
57
+ delete: (path) => fetch(path, getOptions('DELETE')),
58
+ };
59
+ })();
@@ -8,8 +8,6 @@ const DateFormatter = new Intl.DateTimeFormat('en-US', {
8
8
  day: '2-digit',
9
9
  });
10
10
 
11
- const formatDate = (date) => DateFormatter.format(new Date(date));
12
-
13
11
  const HTML_ESCAPES = {
14
12
  '&': '&amp;',
15
13
  '<': '&lt;',
@@ -18,15 +16,29 @@ const HTML_ESCAPES = {
18
16
  "'": '&#39;',
19
17
  };
20
18
 
21
- // User data is untrusted, so escape it before it goes into innerHTML
22
- const esc = (val) => String(val).replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch]);
19
+ // ========================================================================= //
20
+ // TYPES //
21
+ // ========================================================================= //
22
+
23
+ /**
24
+ * A user record as returned by the API.
25
+ *
26
+ * @typedef {object} User
27
+ * @property {string} id
28
+ * @property {string} name
29
+ * @property {string} email
30
+ * @property {string} created - ISO 8601 date string.
31
+ */
23
32
 
24
33
  // ========================================================================= //
25
34
  // FUNCTIONS //
26
35
  // ========================================================================= //
27
36
 
28
37
  /**
29
- * Render users
38
+ * Build the HTML for the users list, including each user's edit form.
39
+ *
40
+ * @param {User[]} users
41
+ * @returns {string} HTML to assign to `innerHTML`.
30
42
  */
31
43
  function renderUsers(users) {
32
44
  return users
@@ -88,3 +100,29 @@ function renderUsers(users) {
88
100
  })
89
101
  .join('');
90
102
  }
103
+
104
+ // ================================ Helpers ================================ //
105
+
106
+ /**
107
+ * Format a date as MM/DD/YYYY.
108
+ *
109
+ * Used by: {@link renderUsers}
110
+ *
111
+ * @param {string | number | Date} date - Anything `new Date()` accepts.
112
+ * @returns {string}
113
+ */
114
+ function formatDate(date) {
115
+ return DateFormatter.format(new Date(date));
116
+ }
117
+
118
+ /**
119
+ * User data is untrusted, so escape it before it goes into innerHTML.
120
+ *
121
+ * Used by: {@link renderUsers}
122
+ *
123
+ * @param {unknown} val - Converted to a string before escaping.
124
+ * @returns {string}
125
+ */
126
+ function esc(val) {
127
+ return String(val).replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch]);
128
+ }