@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,93 @@
1
+ <% if (educational) { %>// File overview: Creates the target PostgreSQL database if it does not already exist.
2
+ <% } %>
3
+ require('dotenv').config();
4
+
5
+ const { Pool } = require('pg');
6
+
7
+ async function run() {
8
+ const databaseUrl = process.env.DATABASE_URL;
9
+
10
+ if (!databaseUrl) {
11
+ console.error('Error: DATABASE_URL is not set.');
12
+ console.error('Make sure you have a .env file with DATABASE_URL defined.');
13
+ console.error('You can copy .env.example to .env to get started.');
14
+ process.exit(1);
15
+ }
16
+
17
+ <% if (educational) { %> // Extract the database name from DATABASE_URL.
18
+ // This is the path segment that appears after host and port.
19
+ <% } %> const url = new URL(databaseUrl);
20
+ const dbName = url.pathname.slice(1);
21
+
22
+ if (!dbName) {
23
+ console.error('Error: DATABASE_URL does not contain a database name.');
24
+ process.exit(1);
25
+ }
26
+
27
+ <% if (educational) { %> // Validate the database name before interpolation.
28
+ // SQL identifiers cannot use parameter placeholders, so we restrict
29
+ // allowed characters to a safe subset.
30
+ <% } %> if (!/^[a-zA-Z0-9_-]+$/.test(dbName)) {
31
+ console.error(`Error: Database name "${dbName}" contains invalid characters.`);
32
+ console.error('Use only letters, numbers, hyphens, and underscores.');
33
+ process.exit(1);
34
+ }
35
+
36
+ <% if (educational) { %> // Connect to the default "postgres" database, then create the target one.
37
+ <% } %> url.pathname = '/postgres';
38
+ const pool = new Pool({ connectionString: url.toString() });
39
+
40
+ try {
41
+ const result = await pool.query(
42
+ 'SELECT 1 FROM pg_database WHERE datname = $1',
43
+ [dbName]
44
+ );
45
+
46
+ if (result.rows.length > 0) {
47
+ console.log(`Database "${dbName}" already exists.`);
48
+ return;
49
+ }
50
+
51
+ await pool.query(`CREATE DATABASE "${dbName}"`);
52
+ console.log(`Database "${dbName}" created.`);
53
+ } finally {
54
+ await pool.end();
55
+ }
56
+ }
57
+
58
+ run().catch((error) => {
59
+ const message =
60
+ error && typeof error === 'object' && typeof error.message === 'string'
61
+ ? error.message
62
+ : typeof error === 'string'
63
+ ? error
64
+ : String(error);
65
+ const code =
66
+ error && typeof error === 'object' && typeof error.code === 'string'
67
+ ? error.code
68
+ : '';
69
+
70
+ console.error('Failed to create database:', message);
71
+ console.error('');
72
+
73
+ const authError = message.includes('password') || message.includes('SCRAM');
74
+ const connectionRefused =
75
+ message.includes('ECONNREFUSED') || code === 'ECONNREFUSED';
76
+
77
+ if (authError) {
78
+ console.error('Your DATABASE_URL is missing credentials or the password is wrong.');
79
+ console.error('Update DATABASE_URL in your .env file:');
80
+ console.error(' DATABASE_URL=postgres://USER:PASSWORD@localhost:5432/<%= databaseName %>');
81
+ } else if (connectionRefused) {
82
+ console.error('PostgreSQL is not running. Start it first:');
83
+ console.error(' Linux: sudo systemctl start postgresql');
84
+ console.error(' macOS: brew services start postgresql');
85
+ } else {
86
+ console.error('Common fixes:');
87
+ console.error(' - Make sure PostgreSQL is running');
88
+ console.error(' - Check that your user has permission to create databases');
89
+ console.error(' - Verify DATABASE_URL in your .env file is correct');
90
+ }
91
+
92
+ process.exit(1);
93
+ });
@@ -0,0 +1,40 @@
1
+ <% if (educational) { %>// File overview: Executes the reset workflow by running setup and seed scripts in order.
2
+ <% } %>
3
+ const { spawn } = require('node:child_process');
4
+
5
+ <% if (educational) { %>// Run an npm script and stream output to this terminal.
6
+ // Wrapping spawn in a Promise lets us await each step in order.
7
+ <% } %>function run(command, args) {
8
+ return new Promise((resolve, reject) => {
9
+ const child = spawn(command, args, {
10
+ stdio: 'inherit',
11
+ shell: true
12
+ });
13
+
14
+ child.on('exit', (code) => {
15
+ if (code === 0) {
16
+ resolve();
17
+ return;
18
+ }
19
+
20
+ reject(new Error(`${command} ${args.join(' ')} failed with exit code ${code}`));
21
+ });
22
+
23
+ child.on('error', reject);
24
+ });
25
+ }
26
+
27
+ async function main() {
28
+ <% if (isDocker) { %><% if (educational) { %> // Restart the container so reset starts from a clean database state.
29
+ <% } %> await run('npm', ['run', 'db:down']);
30
+ await run('npm', ['run', 'db:up']);
31
+ <% } %><% if (educational) { %> // Recreate tables first, then insert the sample data.
32
+ <% } %> await run('npm', ['run', 'db:setup']);
33
+ await run('npm', ['run', 'db:seed']);
34
+ }
35
+
36
+ main().catch((error) => {
37
+ <% if (educational) { %> // Child scripts print detailed logs, so this is a short summary.
38
+ <% } %> console.error(error.message);
39
+ process.exit(1);
40
+ });
@@ -0,0 +1,62 @@
1
+ <% if (educational) { %>// File overview: Runs db/seed.sql to insert starter rows into the database.
2
+ <% } %>
3
+ require('dotenv').config();
4
+
5
+ const fs = require('node:fs/promises');
6
+ const path = require('node:path');
7
+ const { Pool } = require('pg');
8
+ <% if (isDocker) { %>
9
+ const RETRIES = 20;
10
+ const RETRY_DELAY_MS = 1000;
11
+
12
+ <% if (educational) { %>// Sleep helper used between retry attempts.
13
+ <% } %>function sleep(ms) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+
17
+ <% if (educational) { %>// Retry a simple query until PostgreSQL is ready.
18
+ <% } %>async function waitForDatabase(pool) {
19
+ for (let attempt = 1; attempt <= RETRIES; attempt += 1) {
20
+ try {
21
+ await pool.query('SELECT 1');
22
+ return;
23
+ } catch {
24
+ console.log('Waiting for database...');
25
+ await sleep(RETRY_DELAY_MS);
26
+ }
27
+ }
28
+
29
+ throw new Error('Unable to connect to database after 20 retries.');
30
+ }
31
+ <% } %>
32
+ <% if (educational) { %>// Read db/seed.sql and run it to insert starter data.
33
+ <% } %>async function run() {
34
+ const databaseUrl = process.env.DATABASE_URL;
35
+
36
+ if (!databaseUrl) {
37
+ console.error('Error: DATABASE_URL is not set.');
38
+ console.error('Make sure you have a .env file with DATABASE_URL defined.');
39
+ console.error('You can copy .env.example to .env to get started.');
40
+ process.exit(1);
41
+ }
42
+
43
+ const pool = new Pool({ connectionString: databaseUrl });
44
+
45
+ try {
46
+ <% if (isDocker) { %><% if (educational) { %> // Docker containers may need a few seconds before accepting connections.
47
+ <% } %> await waitForDatabase(pool);
48
+ <% } %><% if (educational) { %> // Loading SQL from a file keeps sample data updates easy to review.
49
+ <% } %> const seedPath = path.join(process.cwd(), 'db', 'seed.sql');
50
+ const seedSql = await fs.readFile(seedPath, 'utf8');
51
+ await pool.query(seedSql);
52
+ console.log('Database seed applied.');
53
+ } finally {
54
+ await pool.end();
55
+ }
56
+ }
57
+
58
+ run().catch((error) => {
59
+ <% if (educational) { %> // Exit with a non-zero status so npm reports the script as failed.
60
+ <% } %> console.error(error.message);
61
+ process.exit(1);
62
+ });
@@ -0,0 +1,62 @@
1
+ <% if (educational) { %>// File overview: Runs db/schema.sql to create or reset database tables.
2
+ <% } %>
3
+ require('dotenv').config();
4
+
5
+ const fs = require('node:fs/promises');
6
+ const path = require('node:path');
7
+ const { Pool } = require('pg');
8
+ <% if (isDocker) { %>
9
+ const RETRIES = 20;
10
+ const RETRY_DELAY_MS = 1000;
11
+
12
+ <% if (educational) { %>// Sleep helper used between retry attempts while the container starts.
13
+ <% } %>function sleep(ms) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+
17
+ <% if (educational) { %>// Retry a simple query until PostgreSQL is ready to accept commands.
18
+ <% } %>async function waitForDatabase(pool) {
19
+ for (let attempt = 1; attempt <= RETRIES; attempt += 1) {
20
+ try {
21
+ await pool.query('SELECT 1');
22
+ return;
23
+ } catch {
24
+ console.log('Waiting for database...');
25
+ await sleep(RETRY_DELAY_MS);
26
+ }
27
+ }
28
+
29
+ throw new Error('Unable to connect to database after 20 retries.');
30
+ }
31
+ <% } %>
32
+ <% if (educational) { %>// Read db/schema.sql and apply it to the configured database.
33
+ <% } %>async function run() {
34
+ const databaseUrl = process.env.DATABASE_URL;
35
+
36
+ if (!databaseUrl) {
37
+ console.error('Error: DATABASE_URL is not set.');
38
+ console.error('Make sure you have a .env file with DATABASE_URL defined.');
39
+ console.error('You can copy .env.example to .env to get started.');
40
+ process.exit(1);
41
+ }
42
+
43
+ const pool = new Pool({ connectionString: databaseUrl });
44
+
45
+ try {
46
+ <% if (isDocker) { %><% if (educational) { %> // Docker containers may need a few seconds before accepting connections.
47
+ <% } %> await waitForDatabase(pool);
48
+ <% } %><% if (educational) { %> // Loading SQL from a file keeps schema changes visible in version control.
49
+ <% } %> const schemaPath = path.join(process.cwd(), 'db', 'schema.sql');
50
+ const schemaSql = await fs.readFile(schemaPath, 'utf8');
51
+ await pool.query(schemaSql);
52
+ console.log('Database schema applied.');
53
+ } finally {
54
+ await pool.end();
55
+ }
56
+ }
57
+
58
+ run().catch((error) => {
59
+ <% if (educational) { %> // Exit with a non-zero status so npm reports the script as failed.
60
+ <% } %> console.error(error.message);
61
+ process.exit(1);
62
+ });
@@ -0,0 +1,45 @@
1
+ <% if (educational) { %>// File overview: Builds and configures the Express app (middleware, routes, and error pipeline).
2
+ <% } %>
3
+ import cors from 'cors';
4
+ import express from 'express';
5
+ import helmet from 'helmet';
6
+ import morgan from 'morgan';
7
+
8
+ import errorHandler from './middleware/errorHandler';
9
+ import notFound from './middleware/notFound';
10
+ import healthRouter from './routes/health';
11
+ import usersRouter from './routes/users';
12
+
13
+ const app = express();
14
+
15
+ <% if (educational) { %>// Parse JSON request bodies so route handlers can read data from req.body.
16
+ // Without this middleware, req.body is undefined for JSON requests.
17
+ <% } %>app.use(express.json());
18
+ <% if (educational) { %>// Enable CORS so browser clients on other origins can call this API.
19
+ <% } %>app.use(cors());
20
+ <% if (educational) { %>// Add common HTTP security headers.
21
+ // Helmet applies safe defaults that reduce exposure to common web attacks.
22
+ <% } %>app.use(helmet());
23
+ <% if (educational) { %>// Log each request in development format to make local debugging easier.
24
+ <% } %>app.use(morgan('dev'));
25
+
26
+ <% if (educational) { %>// Provide a simple root endpoint so visiting the API URL in a browser
27
+ // shows available routes instead of an immediate 404 response.
28
+ <% } %>app.get('/', (_req, res) => {
29
+ res.json({
30
+ message: 'API is running',
31
+ endpoints: {
32
+ health: 'GET /health',
33
+ users: 'GET /api/users',
34
+ createUser: 'POST /api/users'
35
+ }
36
+ });
37
+ });
38
+
39
+ app.use('/health', healthRouter);
40
+ app.use('/api/users', usersRouter);
41
+
42
+ app.use(notFound);
43
+ app.use(errorHandler);
44
+
45
+ export default app;
@@ -0,0 +1,31 @@
1
+ <% if (educational) { %>// File overview: Handles /users HTTP requests and delegates business logic to the service layer.
2
+ <% } %>
3
+ import type { RequestHandler } from 'express';
4
+
5
+ import usersService from '../services/usersService';
6
+
7
+ <% if (educational) { %>// Controllers translate HTTP requests into service calls and convert service
8
+ // results back into HTTP responses. Business rules remain in the service layer.
9
+ <% } %>const listUsers: RequestHandler = async (_req, res, next) => {
10
+ try {
11
+ const users = await usersService.listUsers();
12
+ res.status(200).json(users);
13
+ } catch (error) {
14
+ <% if (educational) { %> // Pass errors to next(error) so the central error handler can respond.
15
+ <% } %> next(error);
16
+ }
17
+ };
18
+
19
+ const createUser: RequestHandler = async (req, res, next) => {
20
+ try {
21
+ const user = await usersService.createUser(req.body ?? {});
22
+ res.status(201).json(user);
23
+ } catch (error) {
24
+ next(error);
25
+ }
26
+ };
27
+
28
+ export default {
29
+ listUsers,
30
+ createUser
31
+ };
@@ -0,0 +1,17 @@
1
+ <% if (educational) { %>// File overview: Creates and exports a shared PostgreSQL connection pool for repository queries.
2
+ <% } %>
3
+ <% if (educational) { %>// Create one shared connection pool for the whole application.
4
+ // Reusing connections is faster and avoids exhausting database limits.
5
+ <% } %>import { Pool } from 'pg';
6
+
7
+ const databaseUrl = process.env.DATABASE_URL;
8
+
9
+ if (!databaseUrl) {
10
+ throw new Error('DATABASE_URL is required for Postgres mode.');
11
+ }
12
+
13
+ const pool = new Pool({
14
+ connectionString: databaseUrl
15
+ });
16
+
17
+ export default pool;
@@ -0,0 +1,14 @@
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
+ <% } %>export default class AppError extends Error {
8
+ status: number;
9
+
10
+ constructor(status: number, message: string) {
11
+ super(message);
12
+ this.status = status;
13
+ }
14
+ }
@@ -0,0 +1,49 @@
1
+ <% if (educational) { %>// File overview: Converts thrown errors into consistent JSON HTTP responses for clients.
2
+ <% } %>
3
+ import type { ErrorRequestHandler } from 'express';
4
+
5
+ interface HttpError extends Error {
6
+ status?: number;
7
+ code?: string;
8
+ detail?: string;
9
+ }
10
+
11
+ <% if (educational) { %>// Express treats middleware with four parameters as an error handler.
12
+ // Any error passed to next(error) in the app is routed to this function.
13
+ <% } %>const errorHandler: ErrorRequestHandler = (error: HttpError, _req, res, _next) => {
14
+ <% if (educational) { %> // PostgreSQL error code 23505 means a UNIQUE constraint was violated.
15
+ // Returning HTTP 409 tells clients that the request conflicts with existing data.
16
+ <% } %> if (error?.code === '23505') {
17
+ const detail = typeof error?.detail === 'string' ? error.detail : '';
18
+ const message = detail.includes('email')
19
+ ? 'A user with this email already exists.'
20
+ : 'A record with this value already exists.';
21
+
22
+ return res.status(409).json({
23
+ status: 409,
24
+ message
25
+ });
26
+ }
27
+
28
+ const status = Number.isInteger(error?.status) ? (error.status as number) : 500;
29
+ const message = typeof error?.message === 'string' ? error.message : 'Internal server error.';
30
+
31
+ const payload: {
32
+ status: number;
33
+ message: string;
34
+ stack?: string;
35
+ } = {
36
+ status,
37
+ message
38
+ };
39
+
40
+ <% if (educational) { %> // Include stack traces in development to speed up debugging.
41
+ // Omit them in production so internal implementation details stay private.
42
+ <% } %> if (process.env.NODE_ENV === 'development' && typeof error?.stack === 'string') {
43
+ payload.stack = error.stack;
44
+ }
45
+
46
+ res.status(status).json(payload);
47
+ };
48
+
49
+ export default errorHandler;
@@ -0,0 +1,13 @@
1
+ <% if (educational) { %>// File overview: Handles unmatched routes by forwarding a 404 error to the central error handler.
2
+ <% } %>
3
+ import type { RequestHandler } from 'express';
4
+
5
+ import AppError from '../errors/AppError';
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
+ <% } %>const notFound: RequestHandler = (_req, _res, next) => {
10
+ next(new AppError(404, 'Route not found.'));
11
+ };
12
+
13
+ export default notFound;
@@ -0,0 +1,87 @@
1
+ <% if (educational) { %>// File overview: Encapsulates user data access so controllers/services stay focused on API behavior.
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
+ <% } %>import pool from '../db/pool';
6
+
7
+ interface UserRow {
8
+ id: number;
9
+ name: string;
10
+ email: string | null;
11
+ }
12
+
13
+ interface CreateUserInput {
14
+ name: string;
15
+ email: string | null;
16
+ }
17
+
18
+ async function getAll(): Promise<UserRow[]> {
19
+ const result = await pool.query<UserRow>('SELECT id, name, email FROM users ORDER BY id ASC');
20
+ return result.rows;
21
+ }
22
+
23
+ async function create({ name, email }: CreateUserInput): Promise<UserRow> {
24
+ <% if (educational) { %> // Use parameter placeholders ($1, $2) so pg binds values safely.
25
+ <% } %> const result = await pool.query<UserRow>(
26
+ 'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email',
27
+ [name, email]
28
+ );
29
+
30
+ return result.rows[0];
31
+ }
32
+
33
+ export default {
34
+ getAll,
35
+ create
36
+ };
37
+ <% } else { %><% if (educational) { %>// Keep data access in one place so the rest of the app does not depend on
38
+ // storage details. This in-memory version can be replaced later with a real
39
+ // database by changing this file only.
40
+ <% } %>export interface User {
41
+ id: number;
42
+ name: string;
43
+ email: string | null;
44
+ }
45
+
46
+ interface CreateUserInput {
47
+ name: string;
48
+ email: string | null;
49
+ }
50
+
51
+ const users: User[] = [
52
+ {
53
+ id: 1,
54
+ name: 'Ada Lovelace',
55
+ email: 'ada@example.com'
56
+ },
57
+ {
58
+ id: 2,
59
+ name: 'Grace Hopper',
60
+ email: 'grace@example.com'
61
+ }
62
+ ];
63
+
64
+ let nextId = users.length + 1;
65
+
66
+ async function getAll(): Promise<User[]> {
67
+ return users;
68
+ }
69
+
70
+ async function create({ name, email }: CreateUserInput): Promise<User> {
71
+ const user: User = {
72
+ id: nextId,
73
+ name,
74
+ email
75
+ };
76
+
77
+ nextId += 1;
78
+ users.push(user);
79
+
80
+ return user;
81
+ }
82
+
83
+ export default {
84
+ getAll,
85
+ create
86
+ };
87
+ <% } %>
@@ -0,0 +1,13 @@
1
+ <% if (educational) { %>// File overview: Registers the health-check route used to confirm the API process is running.
2
+ <% } %>
3
+ import { Router } from 'express';
4
+
5
+ const router = Router();
6
+
7
+ <% if (educational) { %>// Health-check endpoint used by monitors, load balancers, and container
8
+ // platforms to verify that the API process is running.
9
+ <% } %>router.get('/', (_req, res) => {
10
+ res.status(200).json({ ok: true });
11
+ });
12
+
13
+ export default router;
@@ -0,0 +1,14 @@
1
+ <% if (educational) { %>// File overview: Maps /api/users endpoints to controller methods in the MVC request flow.
2
+ <% } %>
3
+ import { Router } from 'express';
4
+
5
+ import usersController from '../controllers/usersController';
6
+
7
+ const router = Router();
8
+
9
+ <% if (educational) { %>// Route files should map URLs to controller functions and nothing more.
10
+ // Keeping them thin makes the API surface quick to scan and reason about.
11
+ <% } %>router.get('/', usersController.listUsers);
12
+ router.post('/', usersController.createUser);
13
+
14
+ export default router;
@@ -0,0 +1,15 @@
1
+ <% if (educational) { %>// File overview: Application entry point that loads environment variables and starts the HTTP server.
2
+ <% } %>
3
+ <% if (educational) { %>// Load variables from .env before other modules read process.env values.
4
+ <% } %>import 'dotenv/config';
5
+
6
+ import app from './app';
7
+ import { getPort } from './utils/getPort';
8
+
9
+ <% if (educational) { %>// Read the server port from PORT, defaulting to 3000 for local development.
10
+ <% } %>const port = getPort(process.env.PORT, 3000);
11
+
12
+ <% if (educational) { %>// Start the HTTP server and listen for incoming requests.
13
+ <% } %>app.listen(port, () => {
14
+ console.log(`Server listening on port ${port}`);
15
+ });
@@ -0,0 +1,35 @@
1
+ <% if (educational) { %>// File overview: Contains user business rules and validation before calling the repository layer.
2
+ <% } %>
3
+ import usersRepository from '../repositories/usersRepository';
4
+ import AppError from '../errors/AppError';
5
+
6
+ interface CreateUserPayload {
7
+ name?: unknown;
8
+ email?: unknown;
9
+ }
10
+
11
+ <% if (educational) { %>// Services contain business rules and input validation. They sit between
12
+ // controllers (HTTP concerns) and repositories (data access concerns).
13
+ <% } %>async function listUsers() {
14
+ return usersRepository.getAll();
15
+ }
16
+
17
+ async function createUser(payload: CreateUserPayload) {
18
+ const { name, email } = payload;
19
+
20
+ <% if (educational) { %> // Validate input before calling the repository. Throwing AppError provides
21
+ // both the message and status code needed for a clear client response.
22
+ <% } %> if (!name || typeof name !== 'string') {
23
+ throw new AppError(400, '"name" is required.');
24
+ }
25
+
26
+ return usersRepository.create({
27
+ name: name.trim(),
28
+ email: typeof email === 'string' ? email.trim() : null
29
+ });
30
+ }
31
+
32
+ export default {
33
+ listUsers,
34
+ createUser
35
+ };
@@ -0,0 +1,12 @@
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
+ <% } %>export function getPort(value: string | undefined, fallback = 3000): number {
5
+ const parsed = Number(value);
6
+
7
+ if (Number.isInteger(parsed) && parsed > 0) {
8
+ return parsed;
9
+ }
10
+
11
+ return fallback;
12
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "node16",
4
+ "moduleResolution": "node16",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src",
7
+ "esModuleInterop": true,
8
+ "strict": true,
9
+ "target": "ES2022",
10
+ "skipLibCheck": true
11
+ },
12
+ "include": ["src"]
13
+ }
@@ -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