Routing in Spin Apps with Hono

By: Thorsten Hans Over the past months, we kept on improving the JavaScript and TypeScript developer experience for Spin. The Spin SDK for JavaScript and its tooling underwent a major overhaul to ensure we meet developers where they are and align with popular patterns, tools and idioms. As part of that journey, we moved the HTTP router capabilities from the Spin SDK for JS. Instead of exporting a router through the Spin SDK, we let users pick and choose their preferred router. In this post, we’ll explore how to use the Hono Router to build full-fledged HTTP APIs with Spin and TypeScript. Meet the Hono Router The Hono Router is a fast, lightweight routing system designed for handling HTTP requests efficiently. It features an intuitive, Express.js-inspired API, making it easy to define routes using methods like app.get(), app.post(), and wildcard patterns. Hono also supports middleware, allowing developers to add functionalities like authentication, logging, and CORS handling seamlessly. With its small footprint and appealing syntax, Hono is an excellent choice for building fast and maintainable Spin apps. Upgrade your Spin Templates Before jumping right into code, you should update your Spin templates. The spin templates upgrade command guides you through the process of updating all your Spin templates. Please ensure to update templates provided by https://github.com/spinframework/spin-js-sdk (and/or https://github.com/fermyon/spin-js-sdk) spin-js-sdk : Upgrading templates from https://github.com/fermyon/spin-js- sdk... Copying remote template source Installing template http-ts... Installing template redis-ts... Installing template redis-js... Installing template http-js... Upgraded 4 template(s) +---------------------------------------------------+ | Name Description | +===================================================+ | http-js HTTP request handler using JavaScript | | http-ts HTTP request handler using TypeScript | | redis-js Redis message handler using JavaScript | | redis-ts Redis message handler using TypeScript | +---------------------------------------------------+ Building a Spin app with TypeScript and Hono Let’s start by creating a new Spin application using the http-ts template: spin new -t http-ts routing-with-hono The spin new command will ask several questions before creating your project, use the answers shown below: Description: Routing with Hono HTTP path: /... HTTP Router: hono Once spin new has created your project, move into the application directory by executing cd routing-with-hono. Exploring the http-ts Template With Hono By instructing spin new to use hono as HTTP router, the template will render the following default implementation (src/index.ts): // For Hono documentation refer to https://hono.dev/docs/ import { Hono } from 'hono' import type { Context, Next } from 'hono' import { logger } from 'hono/logger' let app = new Hono() // Logging to stdout via built-in middleware app.use(logger()) // Example of a custom middleware to set HTTP response header app.use(async (c: Context, next: Next) => { c.header('server', 'Spin CLI') await next() }) app.get('/', (c: Context) => c.text('Hello, Spin!')) app.get('/:name', (c: Context) => { return c.json({ message: `Hello, ${c.req.param('name')}` }) }) app.fire() Here are already some common hono patterns to explore: let app = new Hono(); - Is how you create a new Hono application app.use() - Allows you to register middlewares that are executed before or after your request handler is invoked app.get() - Is used to register a handler for an incoming GET request app.fire() - Adds a global fetch event listener, which acts as the entry point of your Spin application Test the Default Implementation Before modifying the source code and implementing a more realistic application, let’s go ahead and compile and run the application on our local machine. For compiling the source code to WebAssembly (Wasm), we use spin build (or the short variant spin b): spin build spin build will also download necessary dependencies (e.g., the hono NPM package) by invoking npm install for you: Building component routing-with-hono (2 commands) Running build step 1/2 for component routing-with-hono with 'npm install' added 258 packages, and audited 259 packages in 14s 45 packages are looking for funding run `npm fund` for details found 0 vulnerabilities Running build step 2/2 for component routing-with-hono with 'npm run build' > routing-with-hono@1.0.0 build > knitwit --out-dir build/wit/knitwit --out-world combined && npx webpack --mode=production && npx mkdirp dist && npx j2w -i build/bundle.js -d build/wit/knitwit -n combined -o dist/routing-with-hono.wasm Attempting to read knitwit.json loaded configuration for: [ '@fermyon/spin-sdk' ] asset bundle.js 50.3 KiB [emitted] [javascript module] (name: main) orphan modules 51.3 KiB [

May 8, 2025 - 04:24
 0
Routing in Spin Apps with Hono

By: Thorsten Hans
Over the past months, we kept on improving the JavaScript and TypeScript developer experience for Spin. The Spin SDK for JavaScript and its tooling underwent a major overhaul to ensure we meet developers where they are and align with popular patterns, tools and idioms.

As part of that journey, we moved the HTTP router capabilities from the Spin SDK for JS. Instead of exporting a router through the Spin SDK, we let users pick and choose their preferred router. In this post, we’ll explore how to use the Hono Router to build full-fledged HTTP APIs with Spin and TypeScript.

Meet the Hono Router

The Hono Router is a fast, lightweight routing system designed for handling HTTP requests efficiently. It features an intuitive, Express.js-inspired API, making it easy to define routes using methods like app.get(), app.post(), and wildcard patterns. Hono also supports middleware, allowing developers to add functionalities like authentication, logging, and CORS handling seamlessly. With its small footprint and appealing syntax, Hono is an excellent choice for building fast and maintainable Spin apps.

Upgrade your Spin Templates

Before jumping right into code, you should update your Spin templates. The spin templates upgrade command guides you through the process of updating all your Spin templates.

Please ensure to update templates provided by https://github.com/spinframework/spin-js-sdk (and/or https://github.com/fermyon/spin-js-sdk)

spin-js-sdk : Upgrading templates from https://github.com/fermyon/spin-js-
sdk...
Copying remote template source
Installing template http-ts...
Installing template redis-ts...
Installing template redis-js...
Installing template http-js...
Upgraded 4 template(s)
+---------------------------------------------------+
| Name Description |
+===================================================+
| http-js HTTP request handler using JavaScript |
| http-ts HTTP request handler using TypeScript |
| redis-js Redis message handler using JavaScript |
| redis-ts Redis message handler using TypeScript |
+---------------------------------------------------+

Building a Spin app with TypeScript and Hono

Let’s start by creating a new Spin application using the http-ts template:

spin new -t http-ts routing-with-hono

The spin new command will ask several questions before creating your project, use the answers shown below:

Description: Routing with Hono
HTTP path: /...
HTTP Router: hono

Once spin new has created your project, move into the application directory by executing cd routing-with-hono.

Exploring the http-ts Template With Hono

By instructing spin new to use hono as HTTP router, the template will render the following default implementation (src/index.ts):

// For Hono documentation refer to https://hono.dev/docs/
import { Hono } from 'hono'
import type { Context, Next } from 'hono'
import { logger } from 'hono/logger'

let app = new Hono()

// Logging to stdout via built-in middleware
app.use(logger())

// Example of a custom middleware to set HTTP response header
app.use(async (c: Context, next: Next) => {
    c.header('server', 'Spin CLI')
    await next()
})

app.get('/', (c: Context) => c.text('Hello, Spin!'))
app.get('/:name', (c: Context) => {
    return c.json({ message: `Hello, ${c.req.param('name')}` })
})

app.fire()

Here are already some common hono patterns to explore:

  • let app = new Hono(); - Is how you create a new Hono application
  • app.use() - Allows you to register middlewares that are executed before or after your request handler is invoked
  • app.get() - Is used to register a handler for an incoming GET request
  • app.fire() - Adds a global fetch event listener, which acts as the entry point of your Spin application

Test the Default Implementation

Before modifying the source code and implementing a more realistic application, let’s go ahead and compile and run the application on our local machine.

For compiling the source code to WebAssembly (Wasm), we use spin build (or the short variant spin b):

spin build

spin build will also download necessary dependencies (e.g., the hono NPM package) by invoking npm install for you:

Building component routing-with-hono (2 commands)
Running build step 1/2 for component routing-with-hono with 'npm install'

added 258 packages, and audited 259 packages in 14s

45 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

Running build step 2/2 for component routing-with-hono with 'npm run build'

> routing-with-hono@1.0.0 build
> knitwit --out-dir build/wit/knitwit --out-world combined && npx webpack --mode=production && npx mkdirp dist && npx j2w -i build/bundle.js -d build/wit/knitwit -n combined -o dist/routing-with-hono.wasm

Attempting to read knitwit.json
loaded configuration for: [ '@fermyon/spin-sdk' ]
asset bundle.js 50.3 KiB [emitted] [javascript module] (name:
main)
orphan modules 51.3 KiB [orphan] 22 modules
./src/index.ts + 21 modules 51.7 KiB [built] [code generated]
webpack 5.98.0 compiled successfully in 559 ms

Using user-provided wit in: /Users/thorsten/dev/demos/foo/
routing-with-hono/build/wit/knitwit
Component successfully written.
Finished building all Spin components

Once compilation has finished, run spin up, which will start a listener on localhost:3000 (you can specify a different address using the --listen flag):

spin up
logging component stdio to ".spin/logs/"

Serving http://127.0.0.1:3000
Available Routes:
  routing-with-hono: http://127.0.0.1:3000 (wildcard)

From within a new terminal instance, you can send requests to your Spin app using curl:

curl -i localhost:3000

HTTP/1.1 200 OK
server: Spin CLI

content-type: text/plain; charset=UTF-8
content-length: 12
date: Fri, 21 Mar 2025 09:29:49 GMT

Hello, Spin!

You can also invoke the second route (/:name) to verify hono grabbing the value from the :name route parameter:

curl -i localhost:3000/WebAssembly%20Friends

HTTP/1.1 200 OK
server: Spin CLI
content-type: application/json
content-length: 33
date: Fri, 21 Mar 2025 09:31:18 GMT

{"message":"Hello, WebAssembly Friends"}

You can terminate the Spin application by simply pressing CTRL+C.

Implementing a Restful HTTP API with Hono

Let’s build a simple application to demonstrate what a more realistic workload would look like when using hono as the HTTP router in Spin. First, let’s remove the handlers registered by the template and the custom middleware. Your src/index.ts should now look like this:

import { Hono } from 'hono'
import type { Context } from 'hono'
import { logger } from 'hono/logger'

let app = new Hono()

app.use(logger())

app.fire()

We’ll register the following routes with the Hono application (app) before calling app.fire():

  • app.get('/keys', getAllKeys) - To retrieve a list of all keys by sending a GET request
  • app.get('/values/:key', getValueByKey) - To retrieve a particular value using its key by sending a GET request
  • app.post('/values', createValue) - To store a new value by sending a POST request

On top of those routes, we’ll also use the error handling capabilities provided by Hono, and register a global error handler (which will be invoked if our implementation throws an error).

app.onError((err: Error | HTTPResponseError, c: Context) => {
    console.log(`Caught an Error: ${JSON.stringify(err)}`)
    return new Response("Internal Server Error", { status: 500 })
})

With the routes and the global error handler defined, your src/index.ts should now look like this:

import { Hono } from 'hono';
import type { Context } from 'hono'
import { logger } from 'hono/logger'
import { HTTPResponseError } from 'hono/types'

let app = new Hono();

// Logging to stdout via built-in middleware
app.use(logger());

app.get('/keys', getAllKeys)

app.get('/values/:key', getValueByKey)
app.post('/values', createValue)

app.onError((err: Error | HTTPResponseError, c: Context) => {
    console.log(`Caught an Error: ${JSON.stringify(err)}`)
    return new Response("Internal Server Error", { status: 500 })
})

app.fire()

Create a new TypeScript file in the src folder called handlers.ts; it will hold our handlers (getAllKeys, getValueByKey and createValue).

First, let’s implement thegetAllKeys function. We use the Kv APIs provided by the Spin SDK to load all keys from the key-value store and create an HTTP response using the json function defined on the Context instance we receive from Hono:

import { Kv } from '@fermyon/spin-sdk'
import { Context } from 'hono'

export const getAllKeys = (c: Context): Response => {
    const kv = Kv.openDefault()
    const all = kv.getKeys()
    return c.json(all)
}

Next, we implement the getValueByKey handler. We load the route parameter (:key) using the req.param() method provided by the Context instance. Also, we should do some housekeeping here to ensure our app returns

  • an HTTP 400 if the key is not specified or empty
  • an HTTP 404 if the desired key does not exist in the key-value store
export const getValueByKey = (c: Context): Response => {
    const key = c.req.param("key")
    if (!key) {
        return new Response("Bad Request", { status: 400 })
    }
    const kv = Kv.openDefault()
    if (!kv.exists(key)) {
        return new Response("Not Found", { status: 404 })
    }
    return c.json(kv.getJson(key))
}

For implementing the createValue handler, we will also define an interface to validate incoming request payloads against a predefined schema.

interface CreateValuePayload {
    key: string,
    value: string,
}

As interacting with payloads is an asynchronous operation, we declare createValue as async and use req.json() to read the request payload using the Hono Context:

export const createValue = async (c: Context): Promise => {
    let payload: CreateValuePayload
    try {
        payload = await c.req.json()
    }
    catch (err) {
        return new Response("Bad Request", { status: 400 })
    }
    const kv = Kv.openDefault()
    kv.setJson(payload.key, { value: payload.value })
    return new Response(null, { status: 201 })
}

Move to src/index.ts and bring the handlers into scope by adding the following import statement:

import { createValue, getAllKeys, getValueByKey } from './handlers'

Granting Permission to the Spin App

Wasm is secure by default!

Meaning, your application component is not allowed to use external resources (including the key-value store). To grant your application component permissions for using a key-value store, you must update the component configuration table within the application manifest (spin.toml).

Update the component configuration table to this, to grant permissions to the default key-value store:

[component.routing-with-hono]
source = "dist/routing-with-hono.wasm"
exclude_files = ["**/node_modules"]
key_value_stores = ["default"]

Consult the Spin documentation to learn more about granting key-value store permission to application components.

Running and Testing the Spin App

To compile and run the Spin application, we’ll again use the spin build and spin up commands, as we already did at the beginning of this article. However, we could also use another shorthand to do so:

spin up --build

This should generate an output similar to the following and wait for incoming requests on localhost:3000:

Building component routing-with-hono (2 commands)
Running build step 1/2 for component routing-with-hono with 'npm install'

up to date, audited 259 packages in 485ms

45 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
Running build step 2/2 for component routing-with-hono with 'npm run build'

> routing-with-hono@1.0.0 build
> knitwit --out-dir build/wit/knitwit --out-world combined && npx webpack --mode=production && npx mkdirp dist && npx j2w -i build/bundle.js -d build/wit/knitwit -n combined -o dist/routing-with-hono.wasm

Attempting to read knitwit.json
loaded configuration for: [ '@fermyon/spin-sdk' ]
asset bundle.js 53.8 KiB [emitted] [javascript module] (name: main)
orphan modules 70 KiB [orphan] 44 modules
runtime modules 396 bytes 2 modules
./src/index.ts + 24 modules 54 KiB [not cacheable] [built] [code generated]
webpack 5.98.0 compiled successfully in 648 ms

Using user-provided wit in: /Users/thorsten/dev/demos/foo/
routing-with-hono/build/wit/knitwit
Component successfully written.
Finished building all Spin components
Logging component stdio to ".spin/logs/"
Storing default key-value data to ".spin/sqlite_key_value.db".

Serving http://127.0.0.1:3000

Available Routes:
  routing-with-hono: http://127.0.0.1:3000 (wildcard)

Again, let’s use curl to test the different endpoints of our application. Let’s start by listing all keys (/keys):

curl localhost:3000/keys

HTTP/1.1 200 OK
content-type: application/json
content-length: 2

date: Fri, 21 Mar 2025 10:30:53 GMT
[]

Next, let’s store a value using the key 1234 by sending a POST request to /values:

curl -i --json '{"key": "1234", "value": "some value"}' localhost:3000/values

HTTP/1.1 201 Created
content-length: 0
date: Fri, 21 Mar 2025 10:30:27 GMT

Finally, let’s retrieve the value at /values/1234 by sending another GET request:

curl -i localhost:3000/values/1234
HTTP/1.1 200 OK
content-type: application/json
content-length: 22
date: Fri, 21 Mar 2025 10:31:31 GMT

{"value":"some value"}

Once you’re done, you can terminate your application again by pressing CTRL+C.

Recap and Conclusion

In this post, we explored how to use the Hono router with Spin to build efficient and maintainable HTTP APIs in TypeScript. We started by setting up a new Spin app with Hono, examined the default template, and tested the initial implementation. From there, we built a more realistic RESTful API, implemented route handlers using Spin’s key-value store, and configured the necessary permissions. Finally, we tested the API using curl.

By leveraging Hono’s lightweight, expressive routing capabilities alongside Spin, we’ve created a fast, secure, and scalable API.

What’s Next?

If you’re interested in taking your Spin app with Hono further, here are some ideas to explore:

  • Enhance error handling: Implement more sophisticated error handling
  • Add authentication: Integrate authentication middleware for protected routes
  • Explore Hono middlewares: Adding common features like CORS

Check out the Spin documentation and the Hono documentation to dive deeper into these topics.

Try it Yourself!

Now it’s your turn!

Set up your own Spin app with Hono, experiment with different middleware, and build something awesome. If you have any questions or want to share your project and join the discussion in the Fermyon community over on Discord.

Happy coding!