@alexmc2/create-express-api-starter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -0
  3. package/dist/cli.js +930 -0
  4. package/dist/cli.js.map +1 -0
  5. package/package.json +70 -0
  6. package/templates/js/mvc/.env.example.ejs +7 -0
  7. package/templates/js/mvc/.eslintrc.cjs.ejs +24 -0
  8. package/templates/js/mvc/.gitignore.ejs +6 -0
  9. package/templates/js/mvc/README.md.ejs +187 -0
  10. package/templates/js/mvc/__tests__/app.test.js.ejs +51 -0
  11. package/templates/js/mvc/compose.yaml.ejs +13 -0
  12. package/templates/js/mvc/db/schema.sql.ejs +8 -0
  13. package/templates/js/mvc/db/seed.sql.ejs +7 -0
  14. package/templates/js/mvc/jest.config.js.ejs +6 -0
  15. package/templates/js/mvc/package.json.ejs +40 -0
  16. package/templates/js/mvc/scripts/dbCreate.js.ejs +97 -0
  17. package/templates/js/mvc/scripts/dbReset.js.ejs +42 -0
  18. package/templates/js/mvc/scripts/dbSeed.js.ejs +69 -0
  19. package/templates/js/mvc/scripts/dbSetup.js.ejs +69 -0
  20. package/templates/js/mvc/src/app.js.ejs +57 -0
  21. package/templates/js/mvc/src/controllers/usersController.js.ejs +32 -0
  22. package/templates/js/mvc/src/db/pool.js.ejs +19 -0
  23. package/templates/js/mvc/src/errors/AppError.js.ejs +16 -0
  24. package/templates/js/mvc/src/middleware/errorHandler.js.ejs +39 -0
  25. package/templates/js/mvc/src/middleware/notFound.js.ejs +15 -0
  26. package/templates/js/mvc/src/repositories/usersRepository.js.ejs +69 -0
  27. package/templates/js/mvc/src/routes/health.js.ejs +19 -0
  28. package/templates/js/mvc/src/routes/users.js.ejs +22 -0
  29. package/templates/js/mvc/src/server.js.ejs +21 -0
  30. package/templates/js/mvc/src/services/usersService.js.ejs +34 -0
  31. package/templates/js/mvc/src/utils/getPort.js.ejs +18 -0
  32. package/templates/js/simple/.env.example.ejs +7 -0
  33. package/templates/js/simple/.eslintrc.cjs.ejs +24 -0
  34. package/templates/js/simple/.gitignore.ejs +6 -0
  35. package/templates/js/simple/README.md.ejs +182 -0
  36. package/templates/js/simple/__tests__/app.test.js.ejs +51 -0
  37. package/templates/js/simple/compose.yaml.ejs +13 -0
  38. package/templates/js/simple/db/schema.sql.ejs +8 -0
  39. package/templates/js/simple/db/seed.sql.ejs +7 -0
  40. package/templates/js/simple/jest.config.js.ejs +6 -0
  41. package/templates/js/simple/package.json.ejs +40 -0
  42. package/templates/js/simple/scripts/dbCreate.js.ejs +97 -0
  43. package/templates/js/simple/scripts/dbReset.js.ejs +42 -0
  44. package/templates/js/simple/scripts/dbSeed.js.ejs +69 -0
  45. package/templates/js/simple/scripts/dbSetup.js.ejs +69 -0
  46. package/templates/js/simple/src/app.js.ejs +57 -0
  47. package/templates/js/simple/src/db/pool.js.ejs +19 -0
  48. package/templates/js/simple/src/errors/AppError.js.ejs +16 -0
  49. package/templates/js/simple/src/middleware/errorHandler.js.ejs +39 -0
  50. package/templates/js/simple/src/middleware/notFound.js.ejs +15 -0
  51. package/templates/js/simple/src/repositories/usersRepository.js.ejs +69 -0
  52. package/templates/js/simple/src/routes/health.js.ejs +19 -0
  53. package/templates/js/simple/src/routes/users.js.ejs +52 -0
  54. package/templates/js/simple/src/server.js.ejs +21 -0
  55. package/templates/js/simple/src/utils/getPort.js.ejs +18 -0
  56. package/templates/ts/mvc/.env.example.ejs +7 -0
  57. package/templates/ts/mvc/.eslintrc.cjs.ejs +27 -0
  58. package/templates/ts/mvc/.gitignore.ejs +6 -0
  59. package/templates/ts/mvc/README.md.ejs +188 -0
  60. package/templates/ts/mvc/__tests__/app.test.ts.ejs +45 -0
  61. package/templates/ts/mvc/compose.yaml.ejs +13 -0
  62. package/templates/ts/mvc/db/schema.sql.ejs +8 -0
  63. package/templates/ts/mvc/db/seed.sql.ejs +7 -0
  64. package/templates/ts/mvc/jest.config.js.ejs +7 -0
  65. package/templates/ts/mvc/package.json.ejs +51 -0
  66. package/templates/ts/mvc/scripts/dbCreate.js.ejs +93 -0
  67. package/templates/ts/mvc/scripts/dbReset.js.ejs +40 -0
  68. package/templates/ts/mvc/scripts/dbSeed.js.ejs +62 -0
  69. package/templates/ts/mvc/scripts/dbSetup.js.ejs +62 -0
  70. package/templates/ts/mvc/src/app.ts.ejs +45 -0
  71. package/templates/ts/mvc/src/controllers/usersController.ts.ejs +31 -0
  72. package/templates/ts/mvc/src/db/pool.ts.ejs +17 -0
  73. package/templates/ts/mvc/src/errors/AppError.ts.ejs +14 -0
  74. package/templates/ts/mvc/src/middleware/errorHandler.ts.ejs +49 -0
  75. package/templates/ts/mvc/src/middleware/notFound.ts.ejs +13 -0
  76. package/templates/ts/mvc/src/repositories/usersRepository.ts.ejs +87 -0
  77. package/templates/ts/mvc/src/routes/health.ts.ejs +13 -0
  78. package/templates/ts/mvc/src/routes/users.ts.ejs +14 -0
  79. package/templates/ts/mvc/src/server.ts.ejs +15 -0
  80. package/templates/ts/mvc/src/services/usersService.ts.ejs +35 -0
  81. package/templates/ts/mvc/src/utils/getPort.ts.ejs +12 -0
  82. package/templates/ts/mvc/tsconfig.json.ejs +13 -0
  83. package/templates/ts/simple/.env.example.ejs +7 -0
  84. package/templates/ts/simple/.eslintrc.cjs.ejs +27 -0
  85. package/templates/ts/simple/.gitignore.ejs +6 -0
  86. package/templates/ts/simple/README.md.ejs +182 -0
  87. package/templates/ts/simple/__tests__/app.test.ts.ejs +45 -0
  88. package/templates/ts/simple/compose.yaml.ejs +13 -0
  89. package/templates/ts/simple/db/schema.sql.ejs +8 -0
  90. package/templates/ts/simple/db/seed.sql.ejs +7 -0
  91. package/templates/ts/simple/jest.config.js.ejs +7 -0
  92. package/templates/ts/simple/package.json.ejs +51 -0
  93. package/templates/ts/simple/scripts/dbCreate.js.ejs +93 -0
  94. package/templates/ts/simple/scripts/dbReset.js.ejs +40 -0
  95. package/templates/ts/simple/scripts/dbSeed.js.ejs +62 -0
  96. package/templates/ts/simple/scripts/dbSetup.js.ejs +62 -0
  97. package/templates/ts/simple/src/app.ts.ejs +45 -0
  98. package/templates/ts/simple/src/db/pool.ts.ejs +17 -0
  99. package/templates/ts/simple/src/errors/AppError.ts.ejs +14 -0
  100. package/templates/ts/simple/src/middleware/errorHandler.ts.ejs +49 -0
  101. package/templates/ts/simple/src/middleware/notFound.ts.ejs +13 -0
  102. package/templates/ts/simple/src/repositories/usersRepository.ts.ejs +87 -0
  103. package/templates/ts/simple/src/routes/health.ts.ejs +13 -0
  104. package/templates/ts/simple/src/routes/users.ts.ejs +43 -0
  105. package/templates/ts/simple/src/server.ts.ejs +15 -0
  106. package/templates/ts/simple/src/utils/getPort.ts.ejs +12 -0
  107. package/templates/ts/simple/tsconfig.json.ejs +13 -0
@@ -0,0 +1,16 @@
1
+ <% if (educational) { %>// File overview: Defines an AppError class for expected API errors that include HTTP status codes.
2
+ <% } %>
3
+ <% if (educational) { %>// A custom error class for handling API errors. By extending JavaScript's
4
+ // built-in Error class, we ensure that stack traces are generated correctly
5
+ // and that the error handler can distinguish between anticipated API
6
+ // errors and unexpected application failures.
7
+ <% } %>class AppError extends Error {
8
+ constructor(status, message) {
9
+ super(message);
10
+ this.status = status;
11
+ }
12
+ }
13
+
14
+ <% if (isEsm) { %>export default AppError;
15
+ <% } else { %>module.exports = AppError;
16
+ <% } %>
@@ -0,0 +1,39 @@
1
+ <% if (educational) { %>// File overview: Converts thrown errors into consistent JSON HTTP responses for clients.
2
+ <% } %>
3
+ <% if (educational) { %>// Express treats middleware with four parameters as an error handler.
4
+ // Any error passed to next(error) in the app is routed to this function.
5
+ <% } %>function errorHandler(error, _req, res, _next) {
6
+ <% if (educational) { %> // PostgreSQL error code 23505 means a UNIQUE constraint was violated.
7
+ // Returning HTTP 409 tells clients that the request conflicts with existing data.
8
+ <% } %> if (error?.code === '23505') {
9
+ const detail = typeof error?.detail === 'string' ? error.detail : '';
10
+ const message = detail.includes('email')
11
+ ? 'A user with this email already exists.'
12
+ : 'A record with this value already exists.';
13
+
14
+ return res.status(409).json({
15
+ status: 409,
16
+ message
17
+ });
18
+ }
19
+
20
+ const status = Number.isInteger(error?.status) ? error.status : 500;
21
+ const message = typeof error?.message === 'string' ? error.message : 'Internal server error.';
22
+
23
+ const payload = {
24
+ status,
25
+ message
26
+ };
27
+
28
+ <% if (educational) { %> // Include stack traces in development to speed up debugging.
29
+ // Omit them in production so internal implementation details stay private.
30
+ <% } %> if (process.env.NODE_ENV === 'development' && typeof error?.stack === 'string') {
31
+ payload.stack = error.stack;
32
+ }
33
+
34
+ res.status(status).json(payload);
35
+ }
36
+
37
+ <% if (isEsm) { %>export default errorHandler;
38
+ <% } else { %>module.exports = errorHandler;
39
+ <% } %>
@@ -0,0 +1,15 @@
1
+ <% if (educational) { %>// File overview: Handles unmatched routes by forwarding a 404 error to the central error handler.
2
+ <% } %>
3
+ <% if (isEsm) { %>import AppError from '../errors/AppError.js';
4
+ <% } else { %>const AppError = require('../errors/AppError');
5
+ <% } %>
6
+
7
+ <% if (educational) { %>// This middleware runs only when no route matched the request.
8
+ // It forwards a 404 error to the central error handler for a uniform response.
9
+ <% } %>function notFound(_req, _res, next) {
10
+ next(new AppError(404, 'Route not found.'));
11
+ }
12
+
13
+ <% if (isEsm) { %>export default notFound;
14
+ <% } else { %>module.exports = notFound;
15
+ <% } %>
@@ -0,0 +1,69 @@
1
+ <% if (educational) { %>// File overview: Encapsulates user data access so route code stays focused on HTTP concerns.
2
+ <% } %>
3
+ <% if (isPostgres) { %><% if (educational) { %>// Keep SQL statements in the repository so HTTP layers stay focused on requests
4
+ // and responses. This separation makes the code easier to test and maintain.
5
+ <% } %><% if (isEsm) { %>import pool from '../db/pool.js';
6
+ <% } else { %>const pool = require('../db/pool');
7
+ <% } %>
8
+
9
+ async function getAll() {
10
+ const result = await pool.query('SELECT id, name, email FROM users ORDER BY id ASC');
11
+ return result.rows;
12
+ }
13
+
14
+ async function create({ name, email }) {
15
+ <% if (educational) { %> // Use parameter placeholders ($1, $2) so pg binds values safely.
16
+ <% } %> const result = await pool.query(
17
+ 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email',
18
+ [name, email]
19
+ );
20
+
21
+ return result.rows[0];
22
+ }
23
+
24
+ <% if (isEsm) { %>export default {
25
+ <% } else { %>module.exports = {
26
+ <% } %> getAll,
27
+ create
28
+ };
29
+ <% } else { %><% if (educational) { %>// Keep data access in one place so the rest of the app does not depend on
30
+ // storage details. This in-memory version can be replaced later with a real
31
+ // database by changing this file only.
32
+ <% } %>const users = [
33
+ {
34
+ id: 1,
35
+ name: 'Ada Lovelace',
36
+ email: 'ada@example.com'
37
+ },
38
+ {
39
+ id: 2,
40
+ name: 'Grace Hopper',
41
+ email: 'grace@example.com'
42
+ }
43
+ ];
44
+
45
+ let nextId = users.length + 1;
46
+
47
+ async function getAll() {
48
+ return users;
49
+ }
50
+
51
+ async function create({ name, email }) {
52
+ const user = {
53
+ id: nextId,
54
+ name,
55
+ email
56
+ };
57
+
58
+ nextId += 1;
59
+ users.push(user);
60
+
61
+ return user;
62
+ }
63
+
64
+ <% if (isEsm) { %>export default {
65
+ <% } else { %>module.exports = {
66
+ <% } %> getAll,
67
+ create
68
+ };
69
+ <% } %>
@@ -0,0 +1,19 @@
1
+ <% if (educational) { %>// File overview: Registers the health-check route used to confirm the API process is running.
2
+ <% } %>
3
+ <% if (isEsm) { %>import { Router } from 'express';
4
+ <% } else { %>const express = require('express');
5
+ <% } %>
6
+
7
+ <% if (isEsm) { %>const router = Router();
8
+ <% } else { %>const router = express.Router();
9
+ <% } %>
10
+
11
+ <% if (educational) { %>// Health-check endpoint used by monitors, load balancers, and container
12
+ // platforms to verify that the API process is running.
13
+ <% } %>router.get('/', (_req, res) => {
14
+ res.status(200).json({ ok: true });
15
+ });
16
+
17
+ <% if (isEsm) { %>export default router;
18
+ <% } else { %>module.exports = router;
19
+ <% } %>
@@ -0,0 +1,52 @@
1
+ <% if (educational) { %>// File overview: Defines /api/users routes, including request validation and repository calls.
2
+ <% } %>
3
+ <% if (isEsm) { %>import { Router } from 'express';
4
+
5
+ import AppError from '../errors/AppError.js';
6
+ import usersRepository from '../repositories/usersRepository.js';
7
+ <% } else { %>const express = require('express');
8
+
9
+ const usersRepository = require('../repositories/usersRepository');
10
+ const AppError = require('../errors/AppError');
11
+ <% } %>
12
+
13
+ <% if (isEsm) { %>const router = Router();
14
+ <% } else { %>const router = express.Router();
15
+ <% } %>
16
+
17
+ <% if (educational) { %>// Async route handlers should pass failures to next(error) so the
18
+ // centralized error handler can return a consistent HTTP response.
19
+ <% } %>router.get('/', async (_req, res, next) => {
20
+ try {
21
+ const users = await usersRepository.getAll();
22
+ res.status(200).json(users);
23
+ } catch (error) {
24
+ next(error);
25
+ }
26
+ });
27
+
28
+ router.post('/', async (req, res, next) => {
29
+ try {
30
+ <% if (educational) { %> // Validate input at the API boundary before storing data.
31
+ // Calling next(error) skips the rest of the handler and moves control
32
+ // to the central error middleware, which formats the HTTP response.
33
+ <% } %> const { name, email } = req.body ?? {};
34
+
35
+ if (!name || typeof name !== 'string') {
36
+ return next(new AppError(400, '"name" is required.'));
37
+ }
38
+
39
+ const user = await usersRepository.create({
40
+ name: name.trim(),
41
+ email: typeof email === 'string' ? email.trim() : null
42
+ });
43
+
44
+ return res.status(201).json(user);
45
+ } catch (error) {
46
+ return next(error);
47
+ }
48
+ });
49
+
50
+ <% if (isEsm) { %>export default router;
51
+ <% } else { %>module.exports = router;
52
+ <% } %>
@@ -0,0 +1,21 @@
1
+ <% if (educational) { %>// File overview: Application entry point that loads environment variables and starts the HTTP server.
2
+ <% } %>
3
+ <% if (isEsm) { %><% if (educational) { %>// Load variables from .env before other modules read process.env values.
4
+ <% } %>import 'dotenv/config';
5
+
6
+ import app from './app.js';
7
+ import { getPort } from './utils/getPort.js';
8
+ <% } else { %><% if (educational) { %>// Load variables from .env before other modules read process.env values.
9
+ <% } %>require('dotenv').config();
10
+
11
+ const app = require('./app');
12
+ const { getPort } = require('./utils/getPort');
13
+ <% } %>
14
+
15
+ <% if (educational) { %>// Read the server port from PORT, defaulting to 3000 for local development.
16
+ <% } %>const port = getPort(process.env.PORT, 3000);
17
+
18
+ <% if (educational) { %>// Start the HTTP server and listen for incoming requests.
19
+ <% } %>app.listen(port, () => {
20
+ console.log(`Server listening on port ${port}`);
21
+ });
@@ -0,0 +1,18 @@
1
+ <% if (educational) { %>// File overview: Parses PORT into a usable integer with a safe fallback.
2
+ <% } %>
3
+ <% if (educational) { %>// Keep this helper small and pure so entrypoint code stays easy to scan.
4
+ <% } %>function getPort(value, fallback = 3000) {
5
+ const parsed = Number(value);
6
+
7
+ if (Number.isInteger(parsed) && parsed > 0) {
8
+ return parsed;
9
+ }
10
+
11
+ return fallback;
12
+ }
13
+
14
+ <% if (isEsm) { %>export { getPort };
15
+ <% } else { %>module.exports = {
16
+ getPort
17
+ };
18
+ <% } %>
@@ -0,0 +1,7 @@
1
+ PORT=3000
2
+ NODE_ENV=development
3
+ <% if (isPostgres) { %>
4
+ # Update with your Postgres credentials if needed
5
+ # Format: postgres://USER:PASSWORD@HOST:PORT/DATABASE
6
+ DATABASE_URL=<%= databaseUrl %>
7
+ <% } %>
@@ -0,0 +1,27 @@
1
+ module.exports = {
2
+ root: true,
3
+ env: {
4
+ node: true,
5
+ es2022: true,
6
+ jest: true
7
+ },
8
+ parser: '@typescript-eslint/parser',
9
+ parserOptions: {
10
+ ecmaVersion: 'latest',
11
+ sourceType: 'module'
12
+ },
13
+ plugins: ['@typescript-eslint'],
14
+ extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
15
+ ignorePatterns: ['dist/', 'node_modules/', 'coverage/'],
16
+ rules: {
17
+ 'no-undef': 'off',
18
+ 'no-unused-vars': 'off',
19
+ '@typescript-eslint/no-unused-vars': [
20
+ 'error',
21
+ {
22
+ argsIgnorePattern: '^_',
23
+ varsIgnorePattern: '^_'
24
+ }
25
+ ]
26
+ }
27
+ };
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ .env
3
+ dist
4
+ coverage
5
+ npm-debug.log*
6
+ .DS_Store
@@ -0,0 +1,188 @@
1
+ # <%= projectName %>
2
+
3
+ Express API starter generated by `@alexmc2/create-express-api-starter`.
4
+
5
+ ## Selected options
6
+
7
+ - Language: <%= languageLabel %>
8
+ - Dev watcher: tsx watch
9
+ - Architecture: <%= architectureLabel %>
10
+ - Database: <%= databaseLabel %>
11
+ - Educational comments: <%= educationalLabel %>
12
+
13
+ ## Folder structure
14
+
15
+ ```
16
+ .
17
+ ├── src
18
+ │ ├── app.ts
19
+ │ ├── server.ts
20
+ │ ├── routes
21
+ │ │ ├── health.ts
22
+ │ │ └── users.ts
23
+ │ ├── controllers
24
+ │ │ └── usersController.ts
25
+ │ ├── services
26
+ │ │ └── usersService.ts
27
+ │ ├── repositories
28
+ │ │ └── usersRepository.ts
29
+ <% if (isPostgres) { %>│ ├── db
30
+ │ │ └── pool.ts
31
+ <% } %>│ ├── errors
32
+ │ │ └── AppError.ts
33
+ │ ├── utils
34
+ │ │ └── getPort.ts
35
+ │ └── middleware
36
+ │ ├── errorHandler.ts
37
+ │ └── notFound.ts
38
+ ├── __tests__
39
+ │ └── app.test.ts
40
+ <% if (isPostgres) { %>├── db
41
+ │ ├── schema.sql
42
+ │ └── seed.sql
43
+ <% } %><% if (isPostgres) { %>├── scripts
44
+ <% if (isPsql) { %>│ ├── dbCreate.js
45
+ <% } %>│ ├── dbSetup.js
46
+ │ ├── dbSeed.js
47
+ │ └── dbReset.js
48
+ <% } %><% if (isDocker) { %>├── compose.yaml
49
+ <% } %>├── .env.example
50
+ ├── .gitignore
51
+ ├── .eslintrc.cjs
52
+ ├── package.json
53
+ ├── tsconfig.json
54
+ └── jest.config.js
55
+ ```
56
+
57
+ <% if (isPsql) { %>
58
+ ## Prerequisites
59
+
60
+ This project uses a local PostgreSQL database. Follow the steps for your OS below.
61
+
62
+ ### 1. Install PostgreSQL
63
+
64
+ **macOS** (using [Homebrew](https://brew.sh)):
65
+
66
+ ```sh
67
+ brew install postgresql@17
68
+ brew services start postgresql@17
69
+ ```
70
+
71
+ **Ubuntu / Debian**:
72
+
73
+ ```sh
74
+ sudo apt update && sudo apt install postgresql postgresql-contrib
75
+ sudo systemctl start postgresql && sudo systemctl enable postgresql
76
+ ```
77
+
78
+ **Windows**: Download the installer from https://www.postgresql.org/download/windows/ and follow the prompts. Remember the password you set — you'll need it in step 2.
79
+
80
+ ### 2. Set up your database role
81
+
82
+ Your `.env` file connects as `<%= osUsername %>` with password `postgres`. You need a matching PostgreSQL role.
83
+
84
+ **Linux** (run once):
85
+
86
+ ```sh
87
+ sudo -u postgres createuser --createdb "$USER"
88
+ sudo -u postgres psql -c "ALTER USER \"$USER\" WITH PASSWORD 'postgres';"
89
+ ```
90
+
91
+ **macOS**: Homebrew already created a role for you. Run the commands above only if you get an auth error.
92
+
93
+ **Windows**: The installer created a `postgres` role. Edit `DATABASE_URL` in your `.env` to use it:
94
+
95
+ ```
96
+ DATABASE_URL=postgres://postgres:YOUR_INSTALL_PASSWORD@localhost:5432/<%= databaseName %>
97
+ ```
98
+
99
+ ### 3. Verify
100
+
101
+ ```sh
102
+ pg_isready
103
+ ```
104
+
105
+ You should see `accepting connections`.
106
+ <% } %>
107
+ <% if (isDocker) { %>
108
+ ## Prerequisites
109
+
110
+ This project uses PostgreSQL in a Docker container. Install [Docker Desktop](https://docs.docker.com/get-docker/) and make sure it's running before continuing.
111
+ <% } %>
112
+
113
+ ## Run the app
114
+
115
+ ```sh
116
+ npm install
117
+ cp .env.example .env
118
+ <% if (isPsql) { %>npm run db:create
119
+ npm run db:setup
120
+ npm run db:seed
121
+ <% } else if (isDocker) { %>npm run db:up
122
+ npm run db:setup
123
+ npm run db:seed
124
+ <% } %>npm run dev
125
+ ```
126
+
127
+ Run `npm run lint` to check the code with ESLint.
128
+
129
+ ## Add a new route
130
+
131
+ 1. Add a router in `src/routes`.
132
+ 2. Call controller functions from that route.
133
+ 3. Put business rules in `src/services`.
134
+ 4. Put data access code in `src/repositories`.
135
+ 5. Register the route in `src/app.ts`.
136
+
137
+ ## Where business logic goes
138
+
139
+ Use `src/services` for business logic. Controllers translate HTTP requests/responses, and repositories handle persistence.
140
+
141
+ ## How errors work
142
+
143
+ - `src/middleware/notFound.ts` handles unknown routes with a 404 JSON response.
144
+ - `src/middleware/errorHandler.ts` returns `{ status, message }` and includes `stack` only in development.
145
+
146
+ <% if (isPsql) { %>
147
+ ## Database commands
148
+
149
+ | Command | What it does |
150
+ | --- | --- |
151
+ | `npm run db:create` | Creates the database (safe to re-run) |
152
+ | `npm run db:setup` | Applies the schema (creates tables) |
153
+ | `npm run db:seed` | Inserts sample data |
154
+ | `npm run db:reset` | Drops and re-creates tables + sample data |
155
+
156
+ ## Troubleshooting
157
+
158
+ **"connection refused"** — PostgreSQL isn't running.
159
+
160
+ ```sh
161
+ # Linux
162
+ sudo systemctl start postgresql
163
+ # macOS
164
+ brew services start postgresql@17
165
+ ```
166
+
167
+ **"role does not exist"** — Create a Postgres role for your OS user:
168
+
169
+ ```sh
170
+ sudo -u postgres createuser --createdb "$USER"
171
+ sudo -u postgres psql -c "ALTER USER \"$USER\" WITH PASSWORD 'postgres';"
172
+ ```
173
+
174
+ **"password authentication failed" / "client password must be a string"** — The credentials in `DATABASE_URL` are wrong or missing. Run the role setup commands in [Prerequisites](#prerequisites) and make sure `DATABASE_URL` in `.env` matches.
175
+
176
+ **"database does not exist"** — Run `npm run db:create`.
177
+ <% } %>
178
+ <% if (isDocker) { %>
179
+ ## Database commands
180
+
181
+ | Command | What it does |
182
+ | --- | --- |
183
+ | `npm run db:up` | Starts the Postgres Docker container |
184
+ | `npm run db:down` | Stops the container |
185
+ | `npm run db:setup` | Applies the schema (creates tables) |
186
+ | `npm run db:seed` | Inserts sample data |
187
+ | `npm run db:reset` | Stops container, restarts, re-applies schema + seed |
188
+ <% } %>
@@ -0,0 +1,45 @@
1
+ <% if (educational) { %>// File overview: Basic endpoint tests that verify the API responds with expected status codes and payloads.
2
+ <% } %>
3
+ <% if (educational) { %>// Supertest sends HTTP requests directly to the Express app instance.
4
+ // This keeps tests fast and isolated because no network port is opened.
5
+ <% } %>import request from 'supertest';
6
+
7
+ import app from '../src/app';
8
+ <% if (isPostgres) { %>import pool from '../src/db/pool';
9
+ <% } %>
10
+ <% if (isPostgres) { %>afterAll(async () => {
11
+ await pool.end();
12
+ });
13
+
14
+ <% } %>describe('API', () => {
15
+ test('GET / returns API info', async () => {
16
+ const response = await request(app).get('/');
17
+
18
+ expect(response.status).toBe(200);
19
+ expect(response.body.message).toBe('API is running');
20
+ expect(response.body.endpoints).toBeDefined();
21
+ });
22
+
23
+ test('GET /health returns { ok: true }', async () => {
24
+ const response = await request(app).get('/health');
25
+
26
+ expect(response.status).toBe(200);
27
+ expect(response.body).toEqual({ ok: true });
28
+ });
29
+
30
+ test('GET /api/users returns an array', async () => {
31
+ const response = await request(app).get('/api/users');
32
+
33
+ expect(response.status).toBe(200);
34
+ expect(Array.isArray(response.body)).toBe(true);
35
+ });
36
+
37
+ test('POST /api/users with missing name returns 400', async () => {
38
+ const response = await request(app)
39
+ .post('/api/users')
40
+ .send({ email: 'test@example.com' });
41
+
42
+ expect(response.status).toBe(400);
43
+ expect(response.body.message).toBeDefined();
44
+ });
45
+ });
@@ -0,0 +1,13 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:16
4
+ environment:
5
+ POSTGRES_USER: postgres
6
+ POSTGRES_PASSWORD: postgres
7
+ POSTGRES_DB: <%= databaseName %>
8
+ ports:
9
+ - '5433:5432'
10
+ volumes:
11
+ - postgres_data:/var/lib/postgresql/data
12
+ volumes:
13
+ postgres_data:
@@ -0,0 +1,8 @@
1
+ <% if (educational) { %>-- File overview: Creates the tables and constraints required by this starter API.
2
+ <% } %>
3
+ DROP TABLE IF EXISTS users;
4
+ CREATE TABLE users (
5
+ id SERIAL PRIMARY KEY,
6
+ name TEXT NOT NULL,
7
+ email TEXT UNIQUE
8
+ );
@@ -0,0 +1,7 @@
1
+ <% if (educational) { %>-- File overview: Inserts starter rows so the API has sample data on first run.
2
+ <% } %>
3
+ INSERT INTO users (name, email)
4
+ VALUES
5
+ ('Ada Lovelace', 'ada@example.com'),
6
+ ('Grace Hopper', 'grace@example.com'),
7
+ ('Margaret Hamilton', 'margaret@example.com');
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ transform: {
3
+ '^.+\\.tsx?$': ['@swc/jest']
4
+ },
5
+ testEnvironment: 'node'<% if (isPostgres) { %>,
6
+ setupFiles: ['dotenv/config']<% } %>
7
+ };
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "<%= packageName %>",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Express API generated by @alexmc2/create-express-api-starter",
6
+ "scripts": {
7
+ "dev": "tsx watch src/server.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/server.js",
10
+ "test": "jest",
11
+ "lint": "eslint . --ext .ts"<% if (isPsql) { %>,
12
+ "db:create": "node scripts/dbCreate.js",
13
+ "db:setup": "node scripts/dbSetup.js",
14
+ "db:seed": "node scripts/dbSeed.js",
15
+ "db:reset": "node scripts/dbReset.js"<% } %><% if (isDocker) { %>,
16
+ "db:up": "docker compose up -d",
17
+ "db:down": "docker compose down -v",
18
+ "db:setup": "node scripts/dbSetup.js",
19
+ "db:seed": "node scripts/dbSeed.js",
20
+ "db:reset": "node scripts/dbReset.js"<% } %>
21
+ },
22
+ "engines": {
23
+ "node": ">=20.13"
24
+ },
25
+ "dependencies": {
26
+ "cors": "^2.8.5",
27
+ "dotenv": "^16.4.5",
28
+ "express": "^4.21.1",
29
+ "helmet": "^8.0.0",
30
+ "morgan": "^1.10.0"<% if (isPostgres) { %>,
31
+ "pg": "^8.13.1"<% } %>
32
+ },
33
+ "devDependencies": {
34
+ "@swc/core": "^1.7.26",
35
+ "@swc/jest": "^0.2.36",
36
+ "@types/cors": "^2.8.17",
37
+ "@types/express": "^5.0.0",
38
+ "@types/jest": "^29.5.13",
39
+ "@types/morgan": "^1.9.9",
40
+ "@types/node": "^20.16.11",
41
+ <% if (isPostgres) { %> "@types/pg": "^8.11.10",
42
+ <% } %> "@types/supertest": "^6.0.2",
43
+ "@typescript-eslint/eslint-plugin": "^7.18.0",
44
+ "@typescript-eslint/parser": "^7.18.0",
45
+ "eslint": "^8.57.0",
46
+ "jest": "^29.7.0",
47
+ "supertest": "^7.0.0",
48
+ "tsx": "^4.19.2",
49
+ "typescript": "^5.6.3"
50
+ }
51
+ }