@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.
- package/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/cli.js +930 -0
- package/dist/cli.js.map +1 -0
- package/package.json +70 -0
- package/templates/js/mvc/.env.example.ejs +7 -0
- package/templates/js/mvc/.eslintrc.cjs.ejs +24 -0
- package/templates/js/mvc/.gitignore.ejs +6 -0
- package/templates/js/mvc/README.md.ejs +187 -0
- package/templates/js/mvc/__tests__/app.test.js.ejs +51 -0
- package/templates/js/mvc/compose.yaml.ejs +13 -0
- package/templates/js/mvc/db/schema.sql.ejs +8 -0
- package/templates/js/mvc/db/seed.sql.ejs +7 -0
- package/templates/js/mvc/jest.config.js.ejs +6 -0
- package/templates/js/mvc/package.json.ejs +40 -0
- package/templates/js/mvc/scripts/dbCreate.js.ejs +97 -0
- package/templates/js/mvc/scripts/dbReset.js.ejs +42 -0
- package/templates/js/mvc/scripts/dbSeed.js.ejs +69 -0
- package/templates/js/mvc/scripts/dbSetup.js.ejs +69 -0
- package/templates/js/mvc/src/app.js.ejs +57 -0
- package/templates/js/mvc/src/controllers/usersController.js.ejs +32 -0
- package/templates/js/mvc/src/db/pool.js.ejs +19 -0
- package/templates/js/mvc/src/errors/AppError.js.ejs +16 -0
- package/templates/js/mvc/src/middleware/errorHandler.js.ejs +39 -0
- package/templates/js/mvc/src/middleware/notFound.js.ejs +15 -0
- package/templates/js/mvc/src/repositories/usersRepository.js.ejs +69 -0
- package/templates/js/mvc/src/routes/health.js.ejs +19 -0
- package/templates/js/mvc/src/routes/users.js.ejs +22 -0
- package/templates/js/mvc/src/server.js.ejs +21 -0
- package/templates/js/mvc/src/services/usersService.js.ejs +34 -0
- package/templates/js/mvc/src/utils/getPort.js.ejs +18 -0
- package/templates/js/simple/.env.example.ejs +7 -0
- package/templates/js/simple/.eslintrc.cjs.ejs +24 -0
- package/templates/js/simple/.gitignore.ejs +6 -0
- package/templates/js/simple/README.md.ejs +182 -0
- package/templates/js/simple/__tests__/app.test.js.ejs +51 -0
- package/templates/js/simple/compose.yaml.ejs +13 -0
- package/templates/js/simple/db/schema.sql.ejs +8 -0
- package/templates/js/simple/db/seed.sql.ejs +7 -0
- package/templates/js/simple/jest.config.js.ejs +6 -0
- package/templates/js/simple/package.json.ejs +40 -0
- package/templates/js/simple/scripts/dbCreate.js.ejs +97 -0
- package/templates/js/simple/scripts/dbReset.js.ejs +42 -0
- package/templates/js/simple/scripts/dbSeed.js.ejs +69 -0
- package/templates/js/simple/scripts/dbSetup.js.ejs +69 -0
- package/templates/js/simple/src/app.js.ejs +57 -0
- package/templates/js/simple/src/db/pool.js.ejs +19 -0
- package/templates/js/simple/src/errors/AppError.js.ejs +16 -0
- package/templates/js/simple/src/middleware/errorHandler.js.ejs +39 -0
- package/templates/js/simple/src/middleware/notFound.js.ejs +15 -0
- package/templates/js/simple/src/repositories/usersRepository.js.ejs +69 -0
- package/templates/js/simple/src/routes/health.js.ejs +19 -0
- package/templates/js/simple/src/routes/users.js.ejs +52 -0
- package/templates/js/simple/src/server.js.ejs +21 -0
- package/templates/js/simple/src/utils/getPort.js.ejs +18 -0
- package/templates/ts/mvc/.env.example.ejs +7 -0
- package/templates/ts/mvc/.eslintrc.cjs.ejs +27 -0
- package/templates/ts/mvc/.gitignore.ejs +6 -0
- package/templates/ts/mvc/README.md.ejs +188 -0
- package/templates/ts/mvc/__tests__/app.test.ts.ejs +45 -0
- package/templates/ts/mvc/compose.yaml.ejs +13 -0
- package/templates/ts/mvc/db/schema.sql.ejs +8 -0
- package/templates/ts/mvc/db/seed.sql.ejs +7 -0
- package/templates/ts/mvc/jest.config.js.ejs +7 -0
- package/templates/ts/mvc/package.json.ejs +51 -0
- package/templates/ts/mvc/scripts/dbCreate.js.ejs +93 -0
- package/templates/ts/mvc/scripts/dbReset.js.ejs +40 -0
- package/templates/ts/mvc/scripts/dbSeed.js.ejs +62 -0
- package/templates/ts/mvc/scripts/dbSetup.js.ejs +62 -0
- package/templates/ts/mvc/src/app.ts.ejs +45 -0
- package/templates/ts/mvc/src/controllers/usersController.ts.ejs +31 -0
- package/templates/ts/mvc/src/db/pool.ts.ejs +17 -0
- package/templates/ts/mvc/src/errors/AppError.ts.ejs +14 -0
- package/templates/ts/mvc/src/middleware/errorHandler.ts.ejs +49 -0
- package/templates/ts/mvc/src/middleware/notFound.ts.ejs +13 -0
- package/templates/ts/mvc/src/repositories/usersRepository.ts.ejs +87 -0
- package/templates/ts/mvc/src/routes/health.ts.ejs +13 -0
- package/templates/ts/mvc/src/routes/users.ts.ejs +14 -0
- package/templates/ts/mvc/src/server.ts.ejs +15 -0
- package/templates/ts/mvc/src/services/usersService.ts.ejs +35 -0
- package/templates/ts/mvc/src/utils/getPort.ts.ejs +12 -0
- package/templates/ts/mvc/tsconfig.json.ejs +13 -0
- package/templates/ts/simple/.env.example.ejs +7 -0
- package/templates/ts/simple/.eslintrc.cjs.ejs +27 -0
- package/templates/ts/simple/.gitignore.ejs +6 -0
- package/templates/ts/simple/README.md.ejs +182 -0
- package/templates/ts/simple/__tests__/app.test.ts.ejs +45 -0
- package/templates/ts/simple/compose.yaml.ejs +13 -0
- package/templates/ts/simple/db/schema.sql.ejs +8 -0
- package/templates/ts/simple/db/seed.sql.ejs +7 -0
- package/templates/ts/simple/jest.config.js.ejs +7 -0
- package/templates/ts/simple/package.json.ejs +51 -0
- package/templates/ts/simple/scripts/dbCreate.js.ejs +93 -0
- package/templates/ts/simple/scripts/dbReset.js.ejs +40 -0
- package/templates/ts/simple/scripts/dbSeed.js.ejs +62 -0
- package/templates/ts/simple/scripts/dbSetup.js.ejs +62 -0
- package/templates/ts/simple/src/app.ts.ejs +45 -0
- package/templates/ts/simple/src/db/pool.ts.ejs +17 -0
- package/templates/ts/simple/src/errors/AppError.ts.ejs +14 -0
- package/templates/ts/simple/src/middleware/errorHandler.ts.ejs +49 -0
- package/templates/ts/simple/src/middleware/notFound.ts.ejs +13 -0
- package/templates/ts/simple/src/repositories/usersRepository.ts.ejs +87 -0
- package/templates/ts/simple/src/routes/health.ts.ejs +13 -0
- package/templates/ts/simple/src/routes/users.ts.ejs +43 -0
- package/templates/ts/simple/src/server.ts.ejs +15 -0
- package/templates/ts/simple/src/utils/getPort.ts.ejs +12 -0
- 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,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,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,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,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
|
+
}
|