The async shift: services and controllers

The in-memory services were synchronous. Pure functions returning arrays. The moment you swap in a database, everything that touches it becomes async. The query goes over a network connection, waits for Postgres to answer, and resolves a Promise. Your service signatures change. Your controllers change. The structure of the code stays the same.

after/controllers/book.controller.ts
typescript
export async function getBooksHandler(
  req: Request,
  res: Response,
  next: NextFunction
) {
  try {
    const books = await bookService.getAllBooks();
    res.json(books);
  } catch (err) {
    next(err);
  }
}

export async function createBookHandler(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const validation = CreateBookSchema.safeParse(req.body);
  if (!validation.success) {
    res.status(400).json({ error: 'Invalid book data', details: validation.error.format() });
    return;
  }

  try {
    const newBook = await bookService.createBook(validation.data);
    res.status(201).json(newBook);
  } catch (err) {
    next(err);
  }
}

Same controller, now async with try/catch passing errors to next.

The pattern that matters: wrap every async controller body in try/catch and call next(err) on failure. Express then routes the error to the error-handling middleware you wrote earlier. Without try/catch, an unhandled rejection crashes the process. With it, every error has one place to be handled.

You send a Promise object as the JSON body, and the client gets back the empty object {}. It is the most quietly painful bug in async Node, because the error message is unhelpful and the data shape looks almost right. Whenever a response has unexpected empty fields, suspect a missing await.

Quiz: Quiz

Loading practice…

Validation checklist: Persist a book and prove it

Loading practice…

Take a second to look at what you have built. A validating CLI, a working HTTP server, a layered REST API, and a Postgres-backed persistence layer. This is the entire foundation of every backend in production. Everything that comes next (auth, queues, caching, websockets, observability) hangs off this skeleton.

Checkpoint: Foundations checkpoint

Loading practice…