Project Structure

Project Structure and Configuration

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.

The finished tree, with line countsJavaScript
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 stack

app.js exports a factory, never a bound server, and takes its store and logger as arguments:

src/app.js — assembly, and nothing elseJavaScript
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:

Output of 91
$ 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 characters

config.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.