Make an API with every single API documented using OpenAPI specification
Installation
install bun.sh
we will also be using the stoker library that comes with a lot of helper functions built in
bun create hono@latestEslint
pnpm dlx @antfu/eslint-config@latest
for eslint we we are going to use antfu eslint configs
and then bun install
Adding lint scripts
// package.json "scripts": { "dev": "wrangler dev", "lint": "eslint .", "link:fix": "bun run lint --fix", "deploy": "wrangler deploy --minify" },
Add alias in typescript config
// tsconfig.json "paths": { "@/*": [ "./src/*" ] }
Installing @hono/zod-openapi
bun install @hono/zod-openapiwhich is a layer around openapi to describe the schema
import { OpenAPIHono } from '@hono/zod-openapi' const app = new OpenAPIHono() app.get('/', (c) => { return c.text('Hello Hono!') }) export default app
Adding Not Found & Error Handlers
if we go to /not-found or create a error route then hono should handle it by default (see docs) https://hono.dev/docs/api/hono#not-found
so we are going to add them using stoker
bun add stokerand then add this
app.notFound(notFound); // middleware
this will not turn the plain message to a JSON message as we are building a pure JSON api
Error Handler
first try to create a error endpoint
app.get("/error", (c) => { throw new Error("Oh No!"); })
and now add the middleware from stoker for
onErrorAdd Logger
hono has a built in logger but we can use a more pretty standard one using pino
bun add hono-pino pinoand now create a custom middleware using this pino-logger
// src/middlewares/pino-logger.ts import {pinoLogger} from "hono-pino"; export function logger() { return pinoLogger(); } // and then use it in index.ts app.use(logger())
and this will show much detailed logs now
but these request ids are incrementing in order and just in case of horizontal scaling or serverless deploys this will reset so we should think of a better system here
import {pinoLogger} from "hono-pino"; export function logger() { return pinoLogger({ http: { reqId: () => crypto.randomUUID(), } }); }
Custom Pino Logs
so now we also get access to
c.var.logger.info(”Message”) and all the ones are defined here Serving Favicon
browser always makes a request to the favicon.ico file
we can use any emoji as our favicon using serveEmojiFavicon middleware
Disabling Strict Mode
hono differentiates between /hello and /hello/
changing this will fix it
Moving the app to lib/create-app.ts for later testing purposes
// lib/create-app.ts import { OpenAPIHono } from '@hono/zod-openapi' import { logger } from '@/middlewares/pino-logger' import { notFound, onError, serveEmojiFavicon } from 'stoker/middlewares' export default function createApp() { const app = new OpenAPIHono({ strict: false }) app.use(serveEmojiFavicon('🔥')) app.use(logger()) app.notFound(notFound) app.onError(onError) return app; } // now in index.ts import createApp from '@/lib/create-app' const app = createApp() app.get('/hey', (c) => c.text('Route is working!')) export default app
Configure OpenAPI
now we need to create a /doc that will respond with the open api spec page and later on we will create the documentation page for our api
create
lib/configure-openapi.ts // lib/configure-openapi.ts import type { OpenAPIHono } from "@hono/zod-openapi" export default function configureOpenAPI(app: OpenAPIHono) { app.doc('/doc', { openapi: '3.0.0', info: { title: 'Wakati', description: 'Text Intelligence', version: '1.0.0', } }) }
but here we need to make the version stay in sync with our
package.json for which we need to create an entry in our package.json fileand now just to use the openapi spec we have to do this in our server.ts
const app = createApp(); configureOpenApi(app); // pass the app instance we created
which will now provide show robust schema yeah!
Creating Routes!
now we need to create a routes directory to host all of our routes
but for which we again to create an app instance but this time not again instantiating the same logger and favicon middlewares
so we need to make some changes in our
create-app.ts fileimport { OpenAPIHono } from '@hono/zod-openapi' import { logger } from '@/middlewares/pino-logger' import { notFound, onError, serveEmojiFavicon } from 'stoker/middlewares' export function createRouter() { return new OpenAPIHono({ strict: false }) } export default function createApp() { const app = createRouter() app.use(serveEmojiFavicon('🔥')) app.use(logger()) app.notFound(notFound) app.onError(onError) return app; }
notice how we created a createRouter just to get the router file from this file that we can use anytime we want to create a new router
Creating a route
now we can create a route using this method: https://github.com/honojs/middleware/tree/main/packages/zod-openapi
import { createRoute, z } from "@hono/zod-openapi"; import { createRouter } from "@lib/create-app"; const router = createRouter().openapi(createRoute({ method: "get", path: "/", responses: { 200: { content: { 'application/json': { schema: z.object({ message: z.string() }) } }, description: "Wakati Index" } } } )) export default router
this is the schema and now we need to pass the second argument which is going to be our handler
and now for every single route we are first going to document it and then implement it
import { createRoute, z } from "@hono/zod-openapi"; import { createRouter } from "@/lib/create-app"; const router = createRouter().openapi(createRoute({ method: "get", path: "/", responses: { 200: { content: { 'application/json': { schema: z.object({ message: z.string() }) } }, description: "Wakati Index" } } } ), (c) => { return c.json({ message: "Wakati Index" }) }) export default router
and now we are ready to mount this route
Mount Route
import createApp from '@/lib/create-app' import configureOpenAPI from '@/lib/configure-openapi' import index from '@/routes/index.route' const app = createApp() configureOpenAPI(app) // for every route const routes = [ index ]; routes.forEach((route) => app.route("/", route)) app.get('/hey', (c) => c.text('Route is working!')) export default app
AAND Now we will get all the /doc and api responses
Interactive Documentation
tools like SWAGGER UI (OLD) and (Scalar) woooo!
scalar is a paid hosting platform but the docs are opensource and we can use them for free!
which is like postman, thunderclient all built in to our docs wow what a life baby
bun add @scalar/hono-api-referenceimport { apiReference } from '@scalar/hono-api-reference' import type { OpenAPIHono } from "@hono/zod-openapi" import packageJson from '../../package.json' export default function configureOpenAPI(app: OpenAPIHono) { app.doc('/doc', { openapi: '3.0.0', info: { title: 'Wakati', description: 'Text Intelligence', version: packageJson.version, } }) app.get( '/reference', apiReference({ theme: "bluePlanet", spec: { url: '/doc', }, }), ) }
woooo!
and then we can customise it to fit our needs
Customizing Scalar Docs
add a theme and make the default library to js fetch
Serving a custom /favicon.ico file
Making Home Route Proper
create a public folder and host /favicon.ico file inside there and then host static files https://hono.dev/docs/getting-started/cloudflare-workers#serve-static-files