@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,182 @@
|
|
|
1
|
+
# <%= projectName %>
|
|
2
|
+
|
|
3
|
+
Express API starter generated by `@alexmc2/create-express-api-starter`.
|
|
4
|
+
|
|
5
|
+
## Selected options
|
|
6
|
+
|
|
7
|
+
- Language: <%= languageLabel %>
|
|
8
|
+
- Module system: <%= moduleSystemLabel %>
|
|
9
|
+
- Dev watcher: <%= jsDevWatcherLabel %>
|
|
10
|
+
- Architecture: <%= architectureLabel %>
|
|
11
|
+
- Database: <%= databaseLabel %>
|
|
12
|
+
- Educational comments: <%= educationalLabel %>
|
|
13
|
+
|
|
14
|
+
## Folder structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
.
|
|
18
|
+
├── src
|
|
19
|
+
│ ├── app.js
|
|
20
|
+
│ ├── server.js
|
|
21
|
+
│ ├── routes
|
|
22
|
+
│ │ ├── health.js
|
|
23
|
+
│ │ └── users.js
|
|
24
|
+
│ ├── repositories
|
|
25
|
+
│ │ └── usersRepository.js
|
|
26
|
+
<% if (isPostgres) { %>│ ├── db
|
|
27
|
+
│ │ └── pool.js
|
|
28
|
+
<% } %>│ ├── errors
|
|
29
|
+
│ │ └── AppError.js
|
|
30
|
+
│ ├── utils
|
|
31
|
+
│ │ └── getPort.js
|
|
32
|
+
│ └── middleware
|
|
33
|
+
│ ├── errorHandler.js
|
|
34
|
+
│ └── notFound.js
|
|
35
|
+
├── __tests__
|
|
36
|
+
│ └── app.test.js
|
|
37
|
+
<% if (isPostgres) { %>├── db
|
|
38
|
+
│ ├── schema.sql
|
|
39
|
+
│ └── seed.sql
|
|
40
|
+
<% } %><% if (isPostgres) { %>├── scripts
|
|
41
|
+
<% if (isPsql) { %>│ ├── dbCreate.js
|
|
42
|
+
<% } %>│ ├── dbSetup.js
|
|
43
|
+
│ ├── dbSeed.js
|
|
44
|
+
│ └── dbReset.js
|
|
45
|
+
<% } %><% if (isDocker) { %>├── compose.yaml
|
|
46
|
+
<% } %>├── .env.example
|
|
47
|
+
├── .gitignore
|
|
48
|
+
├── .eslintrc.cjs
|
|
49
|
+
├── package.json
|
|
50
|
+
└── jest.config.js
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
<% if (isPsql) { %>
|
|
54
|
+
## Prerequisites
|
|
55
|
+
|
|
56
|
+
This project uses a local PostgreSQL database. Follow the steps for your OS below.
|
|
57
|
+
|
|
58
|
+
### 1. Install PostgreSQL
|
|
59
|
+
|
|
60
|
+
**macOS** (using [Homebrew](https://brew.sh)):
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
brew install postgresql@17
|
|
64
|
+
brew services start postgresql@17
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Ubuntu / Debian**:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
sudo apt update && sudo apt install postgresql postgresql-contrib
|
|
71
|
+
sudo systemctl start postgresql && sudo systemctl enable postgresql
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**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.
|
|
75
|
+
|
|
76
|
+
### 2. Set up your database role
|
|
77
|
+
|
|
78
|
+
Your `.env` file connects as `<%= osUsername %>` with password `postgres`. You need a matching PostgreSQL role.
|
|
79
|
+
|
|
80
|
+
**Linux** (run once):
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
sudo -u postgres createuser --createdb "$USER"
|
|
84
|
+
sudo -u postgres psql -c "ALTER USER \"$USER\" WITH PASSWORD 'postgres';"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**macOS**: Homebrew already created a role for you. Run the commands above only if you get an auth error.
|
|
88
|
+
|
|
89
|
+
**Windows**: The installer created a `postgres` role. Edit `DATABASE_URL` in your `.env` to use it:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
DATABASE_URL=postgres://postgres:YOUR_INSTALL_PASSWORD@localhost:5432/<%= databaseName %>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 3. Verify
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
pg_isready
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
You should see `accepting connections`.
|
|
102
|
+
<% } %>
|
|
103
|
+
<% if (isDocker) { %>
|
|
104
|
+
## Prerequisites
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
<% } %>
|
|
108
|
+
|
|
109
|
+
## Run the app
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
npm install
|
|
113
|
+
cp .env.example .env
|
|
114
|
+
<% if (isPsql) { %>npm run db:create
|
|
115
|
+
npm run db:setup
|
|
116
|
+
npm run db:seed
|
|
117
|
+
<% } else if (isDocker) { %>npm run db:up
|
|
118
|
+
npm run db:setup
|
|
119
|
+
npm run db:seed
|
|
120
|
+
<% } %>npm run dev
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Run `npm run lint` to check the code with ESLint.
|
|
124
|
+
|
|
125
|
+
## Add a new route
|
|
126
|
+
|
|
127
|
+
1. Create a new file in `src/routes`.
|
|
128
|
+
2. Export an Express router from that file.
|
|
129
|
+
3. Register it in `src/app.js` with `app.use('/your-path', yourRouter)`.
|
|
130
|
+
|
|
131
|
+
## Where business logic goes
|
|
132
|
+
|
|
133
|
+
Keep route handlers thin and move data access logic into `src/repositories`. This keeps the API layer easy to read and easy to test.
|
|
134
|
+
|
|
135
|
+
## How errors work
|
|
136
|
+
|
|
137
|
+
- `src/middleware/notFound.js` handles unknown routes with a 404 JSON response.
|
|
138
|
+
- `src/middleware/errorHandler.js` returns a consistent error shape: `{ status, message }` and includes `stack` only in development.
|
|
139
|
+
|
|
140
|
+
<% if (isPsql) { %>
|
|
141
|
+
## Database commands
|
|
142
|
+
|
|
143
|
+
| Command | What it does |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `npm run db:create` | Creates the database (safe to re-run) |
|
|
146
|
+
| `npm run db:setup` | Applies the schema (creates tables) |
|
|
147
|
+
| `npm run db:seed` | Inserts sample data |
|
|
148
|
+
| `npm run db:reset` | Drops and re-creates tables + sample data |
|
|
149
|
+
|
|
150
|
+
## Troubleshooting
|
|
151
|
+
|
|
152
|
+
**"connection refused"** — PostgreSQL isn't running.
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
# Linux
|
|
156
|
+
sudo systemctl start postgresql
|
|
157
|
+
# macOS
|
|
158
|
+
brew services start postgresql@17
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**"role does not exist"** — Create a Postgres role for your OS user:
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
sudo -u postgres createuser --createdb "$USER"
|
|
165
|
+
sudo -u postgres psql -c "ALTER USER \"$USER\" WITH PASSWORD 'postgres';"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**"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.
|
|
169
|
+
|
|
170
|
+
**"database does not exist"** — Run `npm run db:create`.
|
|
171
|
+
<% } %>
|
|
172
|
+
<% if (isDocker) { %>
|
|
173
|
+
## Database commands
|
|
174
|
+
|
|
175
|
+
| Command | What it does |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `npm run db:up` | Starts the Postgres Docker container |
|
|
178
|
+
| `npm run db:down` | Stops the container |
|
|
179
|
+
| `npm run db:setup` | Applies the schema (creates tables) |
|
|
180
|
+
| `npm run db:seed` | Inserts sample data |
|
|
181
|
+
| `npm run db:reset` | Stops container, restarts, re-applies schema + seed |
|
|
182
|
+
<% } %>
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
<% } %><% if (isEsm) { %>import { describe, expect, test<% if (isPostgres) { %>, afterAll<% } %> } from '@jest/globals';
|
|
6
|
+
import request from 'supertest';
|
|
7
|
+
|
|
8
|
+
import app from '../src/app.js';
|
|
9
|
+
<% if (isPostgres) { %>import pool from '../src/db/pool.js';
|
|
10
|
+
<% } %><% } else { %>const request = require('supertest');
|
|
11
|
+
|
|
12
|
+
const app = require('../src/app');
|
|
13
|
+
<% if (isPostgres) { %>const pool = require('../src/db/pool');
|
|
14
|
+
<% } %><% } %>
|
|
15
|
+
<% if (isPostgres) { %>
|
|
16
|
+
afterAll(async () => {
|
|
17
|
+
await pool.end();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
<% } %>describe('API', () => {
|
|
21
|
+
test('GET / returns API info', async () => {
|
|
22
|
+
const response = await request(app).get('/');
|
|
23
|
+
|
|
24
|
+
expect(response.status).toBe(200);
|
|
25
|
+
expect(response.body.message).toBe('API is running');
|
|
26
|
+
expect(response.body.endpoints).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('GET /health returns { ok: true }', async () => {
|
|
30
|
+
const response = await request(app).get('/health');
|
|
31
|
+
|
|
32
|
+
expect(response.status).toBe(200);
|
|
33
|
+
expect(response.body).toEqual({ ok: true });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('GET /api/users returns an array', async () => {
|
|
37
|
+
const response = await request(app).get('/api/users');
|
|
38
|
+
|
|
39
|
+
expect(response.status).toBe(200);
|
|
40
|
+
expect(Array.isArray(response.body)).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('POST /api/users with missing name returns 400', async () => {
|
|
44
|
+
const response = await request(app)
|
|
45
|
+
.post('/api/users')
|
|
46
|
+
.send({ email: 'test@example.com' });
|
|
47
|
+
|
|
48
|
+
expect(response.status).toBe(400);
|
|
49
|
+
expect(response.body.message).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -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,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= packageName %>",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
<% if (isEsm) { %> "type": "module",
|
|
6
|
+
<% } %>
|
|
7
|
+
"description": "Express API generated by @alexmc2/create-express-api-starter",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "<%= jsDevCommand %>",
|
|
10
|
+
"start": "node src/server.js",
|
|
11
|
+
"test": "<% if (isEsm) { %>node --experimental-vm-modules ./node_modules/jest/bin/jest.js<% } else { %>jest<% } %>",
|
|
12
|
+
"lint": "eslint ."<% if (isPsql) { %>,
|
|
13
|
+
"db:create": "node scripts/dbCreate.js",
|
|
14
|
+
"db:setup": "node scripts/dbSetup.js",
|
|
15
|
+
"db:seed": "node scripts/dbSeed.js",
|
|
16
|
+
"db:reset": "node scripts/dbReset.js"<% } %><% if (isDocker) { %>,
|
|
17
|
+
"db:up": "docker compose up -d",
|
|
18
|
+
"db:down": "docker compose down -v",
|
|
19
|
+
"db:setup": "node scripts/dbSetup.js",
|
|
20
|
+
"db:seed": "node scripts/dbSeed.js",
|
|
21
|
+
"db:reset": "node scripts/dbReset.js"<% } %>
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": "<%- useNodemon ? '>=20' : '>=20.13' %>"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"cors": "^2.8.5",
|
|
28
|
+
"dotenv": "^16.4.5",
|
|
29
|
+
"express": "^4.21.1",
|
|
30
|
+
"helmet": "^8.0.0",
|
|
31
|
+
"morgan": "^1.10.0"<% if (isPostgres) { %>,
|
|
32
|
+
"pg": "^8.13.1"<% } %>
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"eslint": "^8.57.0",
|
|
36
|
+
"jest": "^29.7.0",
|
|
37
|
+
<% if (useNodemon) { %> "nodemon": "^3.1.9",
|
|
38
|
+
<% } %> "supertest": "^7.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<% if (educational) { %>// File overview: Creates the target PostgreSQL database if it does not already exist.
|
|
2
|
+
<% } %>
|
|
3
|
+
<% if (isEsm) { %>import 'dotenv/config';
|
|
4
|
+
|
|
5
|
+
import { Pool } from 'pg';
|
|
6
|
+
<% } else { %>require('dotenv').config();
|
|
7
|
+
|
|
8
|
+
const { Pool } = require('pg');
|
|
9
|
+
<% } %>
|
|
10
|
+
|
|
11
|
+
async function run() {
|
|
12
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
13
|
+
|
|
14
|
+
if (!databaseUrl) {
|
|
15
|
+
console.error('Error: DATABASE_URL is not set.');
|
|
16
|
+
console.error('Make sure you have a .env file with DATABASE_URL defined.');
|
|
17
|
+
console.error('You can copy .env.example to .env to get started.');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
<% if (educational) { %> // Extract the database name from DATABASE_URL.
|
|
22
|
+
// This is the path segment that appears after host and port.
|
|
23
|
+
<% } %> const url = new URL(databaseUrl);
|
|
24
|
+
const dbName = url.pathname.slice(1);
|
|
25
|
+
|
|
26
|
+
if (!dbName) {
|
|
27
|
+
console.error('Error: DATABASE_URL does not contain a database name.');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
<% if (educational) { %> // Validate the database name before interpolation.
|
|
32
|
+
// SQL identifiers cannot use parameter placeholders, so we restrict
|
|
33
|
+
// allowed characters to a safe subset.
|
|
34
|
+
<% } %> if (!/^[a-zA-Z0-9_-]+$/.test(dbName)) {
|
|
35
|
+
console.error(`Error: Database name "${dbName}" contains invalid characters.`);
|
|
36
|
+
console.error('Use only letters, numbers, hyphens, and underscores.');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
<% if (educational) { %> // Connect to the default "postgres" database, then create the target one.
|
|
41
|
+
<% } %> url.pathname = '/postgres';
|
|
42
|
+
const pool = new Pool({ connectionString: url.toString() });
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const result = await pool.query(
|
|
46
|
+
'SELECT 1 FROM pg_database WHERE datname = $1',
|
|
47
|
+
[dbName]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (result.rows.length > 0) {
|
|
51
|
+
console.log(`Database "${dbName}" already exists.`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
await pool.query(`CREATE DATABASE "${dbName}"`);
|
|
56
|
+
console.log(`Database "${dbName}" created.`);
|
|
57
|
+
} finally {
|
|
58
|
+
await pool.end();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
run().catch((error) => {
|
|
63
|
+
const message =
|
|
64
|
+
error && typeof error === 'object' && typeof error.message === 'string'
|
|
65
|
+
? error.message
|
|
66
|
+
: typeof error === 'string'
|
|
67
|
+
? error
|
|
68
|
+
: String(error);
|
|
69
|
+
const code =
|
|
70
|
+
error && typeof error === 'object' && typeof error.code === 'string'
|
|
71
|
+
? error.code
|
|
72
|
+
: '';
|
|
73
|
+
|
|
74
|
+
console.error('Failed to create database:', message);
|
|
75
|
+
console.error('');
|
|
76
|
+
|
|
77
|
+
const authError = message.includes('password') || message.includes('SCRAM');
|
|
78
|
+
const connectionRefused =
|
|
79
|
+
message.includes('ECONNREFUSED') || code === 'ECONNREFUSED';
|
|
80
|
+
|
|
81
|
+
if (authError) {
|
|
82
|
+
console.error('Your DATABASE_URL is missing credentials or the password is wrong.');
|
|
83
|
+
console.error('Update DATABASE_URL in your .env file:');
|
|
84
|
+
console.error(' DATABASE_URL=postgres://USER:PASSWORD@localhost:5432/<%= databaseName %>');
|
|
85
|
+
} else if (connectionRefused) {
|
|
86
|
+
console.error('PostgreSQL is not running. Start it first:');
|
|
87
|
+
console.error(' Linux: sudo systemctl start postgresql');
|
|
88
|
+
console.error(' macOS: brew services start postgresql');
|
|
89
|
+
} else {
|
|
90
|
+
console.error('Common fixes:');
|
|
91
|
+
console.error(' - Make sure PostgreSQL is running');
|
|
92
|
+
console.error(' - Check that your user has permission to create databases');
|
|
93
|
+
console.error(' - Verify DATABASE_URL in your .env file is correct');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
process.exit(1);
|
|
97
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<% if (educational) { %>// File overview: Executes the reset workflow by running setup and seed scripts in order.
|
|
2
|
+
<% } %>
|
|
3
|
+
<% if (isEsm) { %>import { spawn } from 'node:child_process';
|
|
4
|
+
<% } else { %>const { spawn } = require('node:child_process');
|
|
5
|
+
<% } %>
|
|
6
|
+
|
|
7
|
+
<% if (educational) { %>// Run an npm script and stream output to this terminal.
|
|
8
|
+
// Wrapping spawn in a Promise lets us await each step in order.
|
|
9
|
+
<% } %>function run(command, args) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn(command, args, {
|
|
12
|
+
stdio: 'inherit',
|
|
13
|
+
shell: true
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
child.on('exit', (code) => {
|
|
17
|
+
if (code === 0) {
|
|
18
|
+
resolve();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
reject(new Error(`${command} ${args.join(' ')} failed with exit code ${code}`));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
child.on('error', reject);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
<% if (isDocker) { %><% if (educational) { %> // Restart the container so reset starts from a clean database state.
|
|
31
|
+
<% } %> await run('npm', ['run', 'db:down']);
|
|
32
|
+
await run('npm', ['run', 'db:up']);
|
|
33
|
+
<% } %><% if (educational) { %> // Recreate tables first, then insert the sample data.
|
|
34
|
+
<% } %> await run('npm', ['run', 'db:setup']);
|
|
35
|
+
await run('npm', ['run', 'db:seed']);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
main().catch((error) => {
|
|
39
|
+
<% if (educational) { %> // Child scripts print detailed logs, so this is a short summary.
|
|
40
|
+
<% } %> console.error(error.message);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<% if (educational) { %>// File overview: Runs db/seed.sql to insert starter rows into the database.
|
|
2
|
+
<% } %>
|
|
3
|
+
<% if (isEsm) { %>import 'dotenv/config';
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { Pool } from 'pg';
|
|
9
|
+
<% } else { %>require('dotenv').config();
|
|
10
|
+
|
|
11
|
+
const fs = require('node:fs/promises');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const { Pool } = require('pg');
|
|
14
|
+
<% } %>
|
|
15
|
+
<% if (isDocker) { %>
|
|
16
|
+
const RETRIES = 20;
|
|
17
|
+
const RETRY_DELAY_MS = 1000;
|
|
18
|
+
|
|
19
|
+
<% if (educational) { %>// Sleep helper used between retry attempts.
|
|
20
|
+
<% } %>function sleep(ms) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
<% if (educational) { %>// Retry a simple query until PostgreSQL is ready.
|
|
25
|
+
<% } %>async function waitForDatabase(pool) {
|
|
26
|
+
for (let attempt = 1; attempt <= RETRIES; attempt += 1) {
|
|
27
|
+
try {
|
|
28
|
+
await pool.query('SELECT 1');
|
|
29
|
+
return;
|
|
30
|
+
} catch {
|
|
31
|
+
console.log('Waiting for database...');
|
|
32
|
+
await sleep(RETRY_DELAY_MS);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw new Error('Unable to connect to database after 20 retries.');
|
|
37
|
+
}
|
|
38
|
+
<% } %>
|
|
39
|
+
<% if (educational) { %>// Read db/seed.sql and run it to insert starter data.
|
|
40
|
+
<% } %>async function run() {
|
|
41
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
42
|
+
|
|
43
|
+
if (!databaseUrl) {
|
|
44
|
+
console.error('Error: DATABASE_URL is not set.');
|
|
45
|
+
console.error('Make sure you have a .env file with DATABASE_URL defined.');
|
|
46
|
+
console.error('You can copy .env.example to .env to get started.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pool = new Pool({ connectionString: databaseUrl });
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
<% if (isDocker) { %><% if (educational) { %> // Docker containers may need a few seconds before accepting connections.
|
|
54
|
+
<% } %> await waitForDatabase(pool);
|
|
55
|
+
<% } %><% if (educational) { %> // Loading SQL from a file keeps sample data updates easy to review.
|
|
56
|
+
<% } %> const seedPath = path.join(process.cwd(), 'db', 'seed.sql');
|
|
57
|
+
const seedSql = await fs.readFile(seedPath, 'utf8');
|
|
58
|
+
await pool.query(seedSql);
|
|
59
|
+
console.log('Database seed applied.');
|
|
60
|
+
} finally {
|
|
61
|
+
await pool.end();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
run().catch((error) => {
|
|
66
|
+
<% if (educational) { %> // Exit with a non-zero status so npm reports the script as failed.
|
|
67
|
+
<% } %> console.error(error.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<% if (educational) { %>// File overview: Runs db/schema.sql to create or reset database tables.
|
|
2
|
+
<% } %>
|
|
3
|
+
<% if (isEsm) { %>import 'dotenv/config';
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
import { Pool } from 'pg';
|
|
9
|
+
<% } else { %>require('dotenv').config();
|
|
10
|
+
|
|
11
|
+
const fs = require('node:fs/promises');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
const { Pool } = require('pg');
|
|
14
|
+
<% } %>
|
|
15
|
+
<% if (isDocker) { %>
|
|
16
|
+
const RETRIES = 20;
|
|
17
|
+
const RETRY_DELAY_MS = 1000;
|
|
18
|
+
|
|
19
|
+
<% if (educational) { %>// Sleep helper used between retry attempts while the container starts.
|
|
20
|
+
<% } %>function sleep(ms) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
<% if (educational) { %>// Retry a simple query until PostgreSQL is ready to accept commands.
|
|
25
|
+
<% } %>async function waitForDatabase(pool) {
|
|
26
|
+
for (let attempt = 1; attempt <= RETRIES; attempt += 1) {
|
|
27
|
+
try {
|
|
28
|
+
await pool.query('SELECT 1');
|
|
29
|
+
return;
|
|
30
|
+
} catch {
|
|
31
|
+
console.log('Waiting for database...');
|
|
32
|
+
await sleep(RETRY_DELAY_MS);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw new Error('Unable to connect to database after 20 retries.');
|
|
37
|
+
}
|
|
38
|
+
<% } %>
|
|
39
|
+
<% if (educational) { %>// Read db/schema.sql and apply it to the configured database.
|
|
40
|
+
<% } %>async function run() {
|
|
41
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
42
|
+
|
|
43
|
+
if (!databaseUrl) {
|
|
44
|
+
console.error('Error: DATABASE_URL is not set.');
|
|
45
|
+
console.error('Make sure you have a .env file with DATABASE_URL defined.');
|
|
46
|
+
console.error('You can copy .env.example to .env to get started.');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pool = new Pool({ connectionString: databaseUrl });
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
<% if (isDocker) { %><% if (educational) { %> // Docker containers may need a few seconds before accepting connections.
|
|
54
|
+
<% } %> await waitForDatabase(pool);
|
|
55
|
+
<% } %><% if (educational) { %> // Loading SQL from a file keeps schema changes visible in version control.
|
|
56
|
+
<% } %> const schemaPath = path.join(process.cwd(), 'db', 'schema.sql');
|
|
57
|
+
const schemaSql = await fs.readFile(schemaPath, 'utf8');
|
|
58
|
+
await pool.query(schemaSql);
|
|
59
|
+
console.log('Database schema applied.');
|
|
60
|
+
} finally {
|
|
61
|
+
await pool.end();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
run().catch((error) => {
|
|
66
|
+
<% if (educational) { %> // Exit with a non-zero status so npm reports the script as failed.
|
|
67
|
+
<% } %> console.error(error.message);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<% if (educational) { %>// File overview: Builds and configures the Express app (middleware, routes, and error pipeline).
|
|
2
|
+
<% } %>
|
|
3
|
+
<% if (isEsm) { %>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.js';
|
|
9
|
+
import notFound from './middleware/notFound.js';
|
|
10
|
+
import healthRouter from './routes/health.js';
|
|
11
|
+
import usersRouter from './routes/users.js';
|
|
12
|
+
<% } else { %>const express = require('express');
|
|
13
|
+
const cors = require('cors');
|
|
14
|
+
const helmet = require('helmet');
|
|
15
|
+
const morgan = require('morgan');
|
|
16
|
+
|
|
17
|
+
const healthRouter = require('./routes/health');
|
|
18
|
+
const usersRouter = require('./routes/users');
|
|
19
|
+
const notFound = require('./middleware/notFound');
|
|
20
|
+
const errorHandler = require('./middleware/errorHandler');
|
|
21
|
+
<% } %>
|
|
22
|
+
|
|
23
|
+
const app = express();
|
|
24
|
+
|
|
25
|
+
<% if (educational) { %>// Parse JSON request bodies so route handlers can read data from req.body.
|
|
26
|
+
// Without this middleware, req.body is undefined for JSON requests.
|
|
27
|
+
<% } %>app.use(express.json());
|
|
28
|
+
<% if (educational) { %>// Enable CORS so browser clients on other origins can call this API.
|
|
29
|
+
<% } %>app.use(cors());
|
|
30
|
+
<% if (educational) { %>// Add common HTTP security headers.
|
|
31
|
+
// Helmet applies safe defaults that reduce exposure to common web attacks.
|
|
32
|
+
<% } %>app.use(helmet());
|
|
33
|
+
<% if (educational) { %>// Log each request in development format to make local debugging easier.
|
|
34
|
+
<% } %>app.use(morgan('dev'));
|
|
35
|
+
|
|
36
|
+
<% if (educational) { %>// Provide a simple root endpoint so visiting the API URL in a browser
|
|
37
|
+
// shows available routes instead of an immediate 404 response.
|
|
38
|
+
<% } %>app.get('/', (_req, res) => {
|
|
39
|
+
res.json({
|
|
40
|
+
message: 'API is running',
|
|
41
|
+
endpoints: {
|
|
42
|
+
health: 'GET /health',
|
|
43
|
+
users: 'GET /api/users',
|
|
44
|
+
createUser: 'POST /api/users'
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
app.use('/health', healthRouter);
|
|
50
|
+
app.use('/api/users', usersRouter);
|
|
51
|
+
|
|
52
|
+
app.use(notFound);
|
|
53
|
+
app.use(errorHandler);
|
|
54
|
+
|
|
55
|
+
<% if (isEsm) { %>export default app;
|
|
56
|
+
<% } else { %>module.exports = app;
|
|
57
|
+
<% } %>
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
<% } %><% if (isEsm) { %>import { Pool } from 'pg';
|
|
6
|
+
<% } else { %>const { Pool } = require('pg');
|
|
7
|
+
<% } %>
|
|
8
|
+
|
|
9
|
+
if (!process.env.DATABASE_URL) {
|
|
10
|
+
throw new Error('DATABASE_URL is required for Postgres mode.');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const pool = new Pool({
|
|
14
|
+
connectionString: process.env.DATABASE_URL
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
<% if (isEsm) { %>export default pool;
|
|
18
|
+
<% } else { %>module.exports = pool;
|
|
19
|
+
<% } %>
|