InteractiveFrameworks

Routing

Give the cats API its routes: a parameter in the path, two parameters, a route that must be registered before the parameterised one, and a DELETE that answers 204.

What you'll learn

  • Read a path parameter with c.req.param('id'), and all of them at once with c.req.param()
  • Register routes for other verbs, and answer 204 with c.body(null, 204)
  • Rely on registration order: Hono tries routes in the order they were added, so a fixed path goes before a parameter that would swallow it

The cats API needs more than fixed paths. A client asks for one cat by its id, deletes another, reads a single field; the path carries which one, and the app has to take that value out of the URL and decide what to do with it. In Hono a route is a verb, a path pattern and a handler, and the pattern is where the URL's variable parts are named.

Parameters in the path

A segment that starts with : is a parameter. It matches exactly one segment, whatever it holds, and the handler reads it by name:

app.get('/users/:id', (c) => {
  const id = c.req.param('id');
  return c.text(`user ${id}`);
});

GET /users/42 gives '42'. Several parameters are read at once with no argument:

app.get('/posts/:postId/comments/:commentId', (c) => {
  const { postId, commentId } = c.req.param();
  return c.json({ postId, commentId });
});

TypeScript knows the names from the path string, so c.req.param('nope') on that route is a type error, and c.req.param('id') is typed string.

Everything in a URL is a string

'42' is the characters 4 and 2. Comparing it with a cat whose id is the number 42 with === is always false, and cats.find((cat) => cat.id === id) finds nothing while looking correct. Convert first: Number(c.req.param('id')). A parameter that is not a number, /users/abc, converts to NaN, which equals nothing, so a lookup simply fails; a later lesson turns that into a proper 400. Parameters arrive URL-decoded, so /users/Ada%20Lovelace gives 'Ada Lovelace'.

Verbs, and a response with no body

Each verb has a method: app.get, app.post, app.put, app.patch, app.delete, and app.all for any verb. app.on('PURGE', '/cache', handler) covers a verb that has no method, and app.on(['PUT', 'PATCH'], '/x', handler) registers one handler for several. A DELETE that succeeded often has nothing to say; the honest answer is 204 No Content, which is c.body(null, 204). Neither c.text('', 204) nor c.json(null, 204) is allowed by the types, because a 204 must not carry a body.

Registration order is priority

Hono tries the routes of a request's verb in the order they were registered and the first handler that answers wins. This matters as soon as a fixed path and a parameter can match the same URL:

app.get('/users/:id', (c) => c.text(`user ${c.req.param('id')}`));
app.get('/users/me', (c) => c.text('you'));

GET /users/me never reaches the second route: :id matches me and answers first. Register /users/me above /users/:id. When two routes must both run, a handler can call await next() to hand over to the next match, which is what middleware does; lesson 4 is about that.

More patterns

  • :id? makes a parameter optional: /cats/:id? matches /cats and /cats/2.
  • :id{[0-9]+} restricts a parameter with a regular expression; /cats/abc then matches nothing at all.
  • * is a wildcard: /files/* matches /files/a/b/c; app.get('*', handler) matches every path, which is how a fallback route is written.
  • Methods chain: app.get('/x', h1).post('/x', h2) registers two routes and returns the app.

Your task

The app in the editor lists three cats. Add the routes the API is missing, on the same array:

  1. GET /cats/newest answers the last cat in the array.
  2. GET /cats/:id answers the cat with that id, or 404 with { "error": "Cat <id> not found" }.
  3. GET /cats/:id/:field answers one field of the cat as plain text, /cats/2/name giving Luna, or 404 with { "error": "Cat <id> has no <field>" } when the cat has no such field.
  4. DELETE /cats/:id removes the cat and answers 204 with no body, or the same 404 as step 2.

Register step 1 before step 2 and read the Endpoints tab for why.

When it fails

  • GET /cats/newest answers { "error": "Cat newest not found" }: the :id route was registered first and took the request. Move /cats/newest above it.
  • GET /cats/2 is a 404 although Luna exists: the parameter is the string '2' and the ids are numbers. Number() before comparing.
  • DELETE /cats/1 answers 200, or the type checker rejects c.text('', 204): a 204 has no body; the helper is c.body(null, 204).
  • GET /cats/2/name answers "Luna" with the quotes: the field was sent with c.json(). Text goes through c.text().

Remember

  • :name in a path is a parameter; c.req.param('name') reads one, c.req.param() reads them all, always as strings.
  • One method per verb, app.on() for the rest, c.body(null, 204) for no content.
  • Routes are tried in registration order and the first handler that answers wins; fixed paths go above the parameters that would swallow them.
  • ?, {regex} and * refine a pattern.
Stuck? Show a hint

Every parameter arrives as a string: Number(c.req.param('id')) before comparing with a cat's id. Array.prototype.find and findIndex do the lookups, splice removes. Register /cats/newest above /cats/:id, or :id will match the word newest first.