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 [

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 incomingGET
request -
app.fire()
- Adds a globalfetch
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 aGET
request -
app.get('/values/:key', getValueByKey)
- To retrieve a particular value using itskey
by sending aGET
request -
app.post('/values', createValue)
- To store a new value by sending aPOST
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!