API foundations
Return the first resource
Create a GET route that returns a predictable JSON collection and an explicit HTTP success response.
The first route returns the list of books. We don’t have a database yet, and I don’t want one yet. An array in memory is enough to test the API layer on its own. The database comes in the third module, once the routes are right.
Open src/app.ts and add the collection route:
import { Hono } from 'hono'
const app = new Hono()
const books = [{ id: '1', title: 'Dune', author: 'Frank Herbert' }]
app.get('/books', c => c.json({ books }))
export default app
c is the Hono context, one object per request that holds the request and helps you build the response. c.json() serializes the value, sets Content-Type: application/json and returns a 200 by default.
Request it with curl -i:
curl -i http://localhost:3000/books
The response should look like this:
HTTP/1.1 200 OK
content-type: application/json
{"books":[{"id":"1","title":"Dune","author":"Frank Herbert"}]}
Why the envelope
Notice I return { books: [...] } and not a bare array. This is the shape we wrote in the design table, and there is a practical reason for it.
A top-level object leaves room to grow. When we add pagination in the third module, the response becomes { books: [...], next: '...' } and no existing client breaks. A bare array has nowhere to put that. Switching from array to object later is a breaking change for every client.
The same rule applies to the empty case. When there are no books, return 200 with { books: [] }. Not 404, not null, not a different shape. An empty collection is a successful answer to “give me the books”.
Check the evidence, not the feeling
When I add a route I confirm three things: the status, the content-type header, and the exact JSON keys. A response that looks right in the browser can still have the wrong status or a missing header. Open the browser network panel too, click the request, and compare the headers tab with what curl printed.
The array is shared state
One more thing to notice. The books array lives in the server process. Every request reads the same array, and when we add POST in the next module, every request writes to it too. That’s convenient for learning.
It is not storage. Restart the dev server and you’re back to one book. Try it now: stop the process, start it again, request /books. Only Dune comes back. That reset is the reason the data later moves behind a database boundary, and seeing it once makes the reason stick.
Lesson completed