InteractiveFrameworks

First steps

Create a Hono app, answer two routes with text and with JSON, and export it so the runtime can call its fetch(), the way Cloudflare Workers, Bun and Deno do.

What you'll learn

  • See that a Hono app is a fetch handler: a Request in, a Response out, the same code on Cloudflare Workers, Bun, Deno, Node and in this tab
  • Register a route with app.get() and answer it with c.text() or c.json()
  • Export the app as the default export, which is what every runtime calls

A web framework usually comes with a server: something that opens a port, accepts connections, parses bytes into a request and calls your code. Hono does not. A Hono app is a function from a Request to a Response, the two objects every modern JavaScript runtime already has, and the runtime you deploy to supplies the server. That one decision is why the same file runs unchanged on Cloudflare Workers, Bun, Deno, Node and, in this tab, inside a Web Worker: the runtime imports your module and calls its fetch. The cats API you will build over this course starts as that one function.

An app is a fetch handler

import { Hono } from 'hono';

const app = new Hono();

app.get('/hello', (c) => c.text('Hello Hono!'));

export default app;

new Hono() creates an app with no routes. app.get(path, handler) adds one: when a GET request arrives whose path matches, the handler runs. There is one such method per HTTP verb, app.post, app.put, app.patch, app.delete, and app.all for any of them.

The handler receives a context, conventionally named c. It wraps the incoming request and carries the helpers that build a response. Whatever the handler returns is the response; here c.text() builds one with a text/plain body. Because the return value is the response, a handler written with braces needs a return:

app.get('/hello', (c) => {
  return c.text('Hello Hono!');
});

The last line, export default app, is not decoration. A Cloudflare Worker is a module whose default export has a fetch(request) method; Bun.serve(app) and Deno's Deno.serve(app.fetch) read the same property. The runtime here does exactly that: it imports main.ts and calls the default export's fetch() for every request the Endpoints tab or the request bar sends. There is no listen() and no port to choose. When the docs say Hono "runs on any runtime that supports Web Standards", this is the whole mechanism.

Answering with text or JSON

Two helpers cover most responses:

app.get('/plain', (c) => c.text('just text'));
app.get('/data', (c) => c.json({ hello: 'world' }));

c.text() sets content-type: text/plain; charset=UTF-8. c.json() serialises its argument with JSON.stringify and sets application/json. Both accept a status code as a second argument, c.json({ error: 'nope' }, 404), and both answer 200 when none is given. A response with no body at all is c.body(null, 204).

What Hono answers when nothing matches is worth seeing once. Request a path no route claims and you get 404 Not Found as plain text, from Hono itself. You get the same 404 when a route matches but its handler returns nothing, because to Hono a handler that produced no response is not the one that answers this request. A forgotten return shows up as a 404, not as an empty 200.

What Run does here

Pressing Run compiles every file in the editor, hands the modules to the runtime, imports the entry and looks at its default export. Then the Endpoints tab calls each graded route and compares what came back. The console shows the learner's console.log output and anything Hono prints, such as the stack of a handler that threw. Nothing else is between your code and the request.

Your task

The app in the editor has no routes and no default export.

  1. Answer GET / with the text Welcome to the cats API.
  2. Answer GET /health with the JSON object { "status": "ok" }.
  3. Export the app as the default export.

Do steps 1 and 2 first and press Run before step 3. The console tells you what a runtime does with a module that exports no app, which is the same message you would meet deploying it.

When it fails

  • main.ts must export default app: the module was imported, but nothing tells the runtime which object answers requests. Add export default app as the last line.
  • GET / → 404 and the body reads 404 Not Found: no route matched. Either the path in app.get() is not /, or the handler never returned the response. (c) => { c.text('…') } calls the helper and throws the result away; drop the braces or add return.
  • GET /health fails with a body like "{\"status\":\"ok\"}": the object was turned into a string by hand and sent with c.text(). c.json() does the serialising and sets the content type the grader checks.
  • A type error on c blocks Run: the handler was declared outside app.get() without a type. Write the arrow function inline, as the examples do, and TypeScript infers the context's type from the route.

Remember

  • A Hono app is a fetch handler: Request in, Response out. The runtime owns the server; export default app is the contract.
  • app.get(path, handler) registers a route; the handler receives the context c and returns the response.
  • c.text() and c.json() build responses; a second argument sets the status.
  • No route answered, or a handler answered nothing, is Hono's own 404 Not Found.
Stuck? Show a hint

app.get(path, handler) registers a route. The handler receives a context c and must return what c.text() or c.json() gives back. The runtime imports main.ts and calls the default export's fetch(), so the file needs export default app at the end.