The layout is Project Layout for an API's, filled in. The line counts show where the weight falls: the layers everyone argues about are tiny, and the data implementation and the tests are the project.
bookshelf/ .env: ACCESS_SECRET, PORT, PAGE_SIZE - never committed
src/ app.js 26 (createApp, never listen) server.js 10 config.js 19 schemas.js 45
errors.js 27 paging.js 16 openapi.js 84 (3.1 doc, generated from schemas.js)
routes/ 69 = books 28 + authors 24 + auth 17 controllers/ 50 = books 28 + reviews 22
services/ 96 = books 40 + reviews 34 + auth 22 data/ 107 = index 9 + memory 98
middleware/ 82 = auth 34 + error-handler 23 + validate 18 + request-id 7
test/api.test.js 219 = 29 Supertest cases against the whole stackapp.js exports a factory, never a bound server, and takes its store and logger as arguments:
export function createApp({ store = createStore(), logger = console } = {}) {
const app = express();
app.disable('x-powered-by');
app.use(requestId); // before the parser: a 400 needs an id too
app.use(express.json({ limit: '32kb' }));
app.use(readToken); // optional auth: sets req.user if a token is sent
app.get('/healthz', (req, res) => res.json({ status: 'ok', store: store.kind ?? 'memory' }));
app.get('/openapi.json', (req, res) => res.json(openapi));
app.use('/api/v1/auth', authRouter(store)); // each router is a factory over the store
app.use('/api/v1/books', booksRouter(store));
app.use('/api/v1/authors', authorsRouter(store));
app.use(notFound); // Section 3.2.7, then Section 3.7.6
app.use(errorHandler(logger));
return app;
}Two orderings were mistakes first. requestId, which reads X-Request-Id or mints one and echoes it back, sat after express.json() until a malformed body proved the point: body-parser rejects the request before the id exists, so the response a client most wants to report arrived with no correlation id. And body-parser's errors carry type and status but no code, so the error handler maps entity.parse.failed and entity.too.large onto codes of yours:
$ curl -si -X POST localhost:4310/api/v1/auth/login -H 'content-type: application/json' -d
'{"email":'
HTTP/1.1 400 Bad Request
X-Request-Id: 088a3f0e
{"error":{"code":"malformed_json","message":"Unexpected end of JSON input"},
"requestId":"088a3f0e"}
$ ACCESS_SECRET=short node src/server.js
configuration error:
ACCESS_SECRET: Too small: expected string to have >=32 charactersconfig.js follows Secrets and Audits: process.loadEnvFile() reads .env with no dependency, and a Zod 44,027 schema gives ACCESS_SECRET no default and a 32-character minimum, so the server cannot boot signing tokens with undefined. server.js closes on SIGINT and SIGTERM.