Hono - API

Build a REST API with Hono, handle routes and path parameters, and connect to a Turso database

Introduction

A web server can be configured as a resource server accessed through a set of “external” functions or endpoints (API).

REST is a fairly common way of interacting between client applications and services using HTTP.

An HTTP request consists of a method and a “path”.

By default, browsers use the GET method, and when you navigate to a website you do so with a GET request and only need to specify the “path”.

But a web API uses other methods, and clients use these methods to perform actions other than obtaining resources.

Work Environment

Create an application (with the netlify template) as explained at Hono - HTML page.

Modify the index.ts file to return a JSON document at the path /api/hello.

app.get('/api/hello', (c) => {
return c.json({
ok: true,
message: 'Hello Hono!',
})
})

Run the server in development mode:

Terminal window
netlify dev

Install curlie:

Terminal window
scoop install curlie

Make a GET request:

Terminal window
curlie localhost:8888/api/hello

You can see that the server returns the JSON response you programmed:

Terminal window
HTTP/1.1 200 OK
Content-Length: 35
Content-Type: application/json
{
"ok": true,
"message": "Hello Hono!"
}

Routes

A web API consists of different “external” functions defined through a “path”.

Path Parameters

You can use part of the path to identify the function and the other part of the path to define the input parameters.

For example, if you have a function that shows the profile of each employee, you can use the path /employee/:id, where /employee is the external name of the function and /:id is the function argument.

index.ts
const employees = [{name: "David"}, {name: "Dora"}]
app.get('/employee/:id', (c) => {
const {id} = c.req.param()
const index = parseInt(id)
const employee = employees[index]
if (!employee) {
return c.json('Employee not found', 404)
} else {
return c.json(employee)
}
})

Path Examples

app.get('/student/:username', (c) => {
const {username} = c.req.param()
return c.json({"student": username})
})
Terminal window
curl http://127.0.0.1:8888/student/eva

Getting a path parameter, URL query value, and appending a Response header is written as follows:

app.get('/post/:id', (c) => {
const page = c.req.query('page')
const id = c.req.param('id')
c.header('X-Message', 'Hi!')
return c.text(`You want to see ${page} of ${id}`)
})

We can easily handle POST, PUT, and DELETE not only GET.

app.post('/post/', (c) => c.text('Created!', 201))
app.delete('/post/:id', (c) =>
c.text(`${c.req.param('id')} is deleted!`)
)

Database

Use Turso to connect to a database.

Install the serverless client:

Terminal window
bun add @tursodatabase/serverless

Create a .env file in the root directory of your project:

TURSO_URL=...
TURSO_TOKEN=...

Use the env() function to retrieve your database credentials from the environment:

import { env } from 'hono/adapter'
import { createClient } from '@tursodatabase/serverless/compat'
app.get('/api/birds', async (c) => {
const { TURSO_URL, TURSO_TOKEN } = env<{ TURSO_URL: string, TURSO_TOKEN: string }>(c)
const client = createClient({ url: TURSO_URL, authToken: TURSO_TOKEN })
const result = await client.execute("SELECT * FROM birds")
return c.json(result.rows)
})

Implementation Examples

Create a database table:

CREATE TABLE birds (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
);

Get a bird by name:

app.get('/api/bird/:name',
describeRoute({description: 'Get a bird by name'}),
async (c) => {
const {name} = c.req.param()
const {TURSO_URL, TURSO_TOKEN} = env<{ TURSO_URL: string, TURSO_TOKEN: string }>(c)
const client = createClient({url: TURSO_URL, authToken: TURSO_TOKEN})
const result = await client.execute("SELECT * FROM birds WHERE name = ?", [name])
if (!result.rows[0]) {
return c.notFound()
}
return c.json(result.rows[0])
})
app.post('/api/bird/',
describeRoute({description: 'Add a bird'}),
async (c) => {
const {name} = await c.req.json()
const {TURSO_URL, TURSO_TOKEN} = env<{ TURSO_URL: string, TURSO_TOKEN: string }>(c)
const client = createClient({url: TURSO_URL, authToken: TURSO_TOKEN})
await client.execute("INSERT INTO birds (name) VALUES (?)", [name])
return c.json({ ok: true, message: 'Bird added' })
})

Netlify

Deploy the project to Cloud - Netlify.

Terminal window
netlify deploy --prod

CI/CD

Deployment can be automated using a GitLab CI/CD pipeline that deploys to Netlify on every push to main.

Add a .gitlab-ci.yml file to the root of your project:

deploy:
stage: deploy
image: node:lts
script:
- npm install -g netlify-cli
- bun install
- bun run build
- netlify deploy --prod --dir dist --auth $NETLIFY_AUTH_TOKEN --site $NETLIFY_SITE_ID

Set the following variables in your GitLab project under Settings → CI/CD → Variables:

VariableDescription
NETLIFY_AUTH_TOKENYour Netlify personal access token
NETLIFY_SITE_IDThe ID of the Netlify site to deploy to

You can find your site ID in the Netlify dashboard under Site configuration → General → Site ID.

Generate a personal access token at User settings → OAuth → Personal access tokens.

OpenAPI

OpenAPI it’s an open standard for describing your APIs, allowing you to provide an API specification encoded in a JSON or YAML document.

It provides a comprehensive dictionary of terms that reflects commonly-understood concepts in the world of APIs, embedding the fundamentals of HTTP and JSON.

hono-openapi is a middleware which enables automatic OpenAPI documentation generation for your Hono API by integrating with validation libraries like Zod, Valibot, ArkType, and TypeBox.

Install the package along with your zod validation library and its dependencies:

Terminal window
bun add hono-openapi @hono/zod-validator zod zod-openapi

Next, define a validation schema for the bird type:

import z from 'zod'
const birdSchema = z.object({
name: z.string()
})

Use describeRoute for route documentation and validation:

import { describeRoute } from 'hono-openapi'
app.post('/api/bird',
describeRoute({ description: 'Add a bird', requestBody: birdSchema}),
async (c) => {
}
)

Add an endpoint for your OpenAPI document:

import { openAPISpecs } from 'hono-openapi'
app.get(
'/api/openapi',
openAPISpecs(app, {
documentation: {
info: {
title: 'Earth API',
version: '1.0.0',
description: 'Earth API',
},
servers: [
{ url: 'http://localhost:8787', description: 'Local Server' },
],
},
})
)

Now, you can access the OpenAPI specification by visiting http://localhost:8080/api/openapi

You can use this specification to generate client libraries, documentation, and more.

Scalar

Scalar example:

Terminal window
bun add @scalar/hono-api-reference

Set up Zod OpenAPI Hono and pass the configured URL to the Scalar middleware:

// // Use the middleware to serve the Scalar API Reference at /scalar
app.get(
"/api",
Scalar({ url: "/api/openapi", })
)

TODO