HTTP and SQLite

Route requests and return JSON

Use Bun.serve routes to match HTTP methods and paths, read path parameters, and return useful JSON responses.

A real API answers different paths differently. /api/notes returns the list, /api/notes/1 returns one note. We could write a chain of if statements on url.pathname inside fetch, but Bun has a better tool.

Bun.serve() accepts a routes object. Bun matches the incoming path against it before it ever calls the fallback fetch handler. So fetch becomes the place for “nothing matched”.

Let’s create two read-only routes for our notes:

type Note = {
  id: number
  title: string
}

const notes: Note[] = [
  { id: 1, title: 'Learn Bun' },
  { id: 2, title: 'Build the notes API' },
]

const server = Bun.serve({
  routes: {
    '/api/notes': {
      GET: () => Response.json(notes),
    },
    '/api/notes/:id': request => {
      const id = Number(request.params.id)
      const note = notes.find(note => note.id === id)

      if (!note) {
        return Response.json(
          { error: 'Note not found' },
          { status: 404 },
        )
      }

      return Response.json(note)
    },
  },
  fetch() {
    return Response.json(
      { error: 'Route not found' },
      { status: 404 },
    )
  },
})

console.log(`Listening on ${server.url}`)

Two shapes of route are in there. /api/notes maps to an object with one handler per HTTP method, here only GET. /api/notes/:id maps to a single function that handles every method.

The :id segment is a path parameter. Whatever appears in that position ends up in request.params.id. It’s always a string, even when it looks like a number, so we convert it before comparing it with the numeric IDs in our array.

Try the collection route:

curl http://localhost:3000/api/notes

You get the whole array:

[{"id":1,"title":"Learn Bun"},{"id":2,"title":"Build the notes API"}]

Then request one note:

curl http://localhost:3000/api/notes/1

That returns {"id":1,"title":"Learn Bun"}.

Now try the failure cases, because they matter as much as the happy path. Ask for /api/notes/99 and you get {"error":"Note not found"} with status 404. Ask for /api/whatever, a path no route matches, and the request falls through to fetch(), which answers with a different 404 body.

Notice that both errors are JSON, with the same error key. A client can handle every failure the same way instead of parsing a different format for each one.

HTTP status codes are part of the API contract. Do not return 200 for every outcome and make clients dig through the body for an error string. A 404 tells the client, and every proxy and monitoring tool in between, exactly what happened.

Lesson completed