Wakati

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@latest

Eslint

pnpm dlx @antfu/eslint-config@latest
for eslint we we are going to use antfu eslint configs
notion image
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-openapi
which 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 stoker
and 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
notion image
notion image

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 onError

Add Logger

hono has a built in logger but we can use a more pretty standard one using pino
bun add hono-pino pino
and 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
notion image
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
notion image
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
notion image
we can use any emoji as our favicon using serveEmojiFavicon middleware

Disabling Strict Mode

notion image
hono differentiates between /hello and /hello/
changing this will fix it
notion image

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 file
notion image
notion image
and 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
notion image
notion image
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 file
import { 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
notion image
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-reference
 
import { 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', }, }), ) }
notion image
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