Skip to main content

Command Palette

Search for a command to run...

Hono: Validation

Updated
8 min read
Hono: Validation
R

Somebody who likes to code

Validation is a critical aspect of building robust APIs. It ensures that incoming data meets expected criteria before processing, preventing bugs, security vulnerabilities, and unexpected behavior. In this article, we'll explore how to implement validation in the application built in our previous post using Zod.

Hono Middlewares

Before diving into validation, it's essential to understand how Hono's middleware system works, as validators are implemented as middleware. Middleware functions are handlers that execute during the request-response cycle.

What is Hono Middleware?

A middleware is a function that accepts two parameters:

  • Context (c) - Provides access to request data, environment bindings, and response utilities.

  • Next (next) - An async function that executes the next middleware in the chain.

The middleware handler interface is defined as:

type MiddlewareHandler = (c: Context, next: Next) => Promise<Response | void>

How does Middleware Work?

The execution flow is:

  • Each middleware is called in order.

  • When await next() is called, the next middleware is executed.

  • After the next() resolves, middleware can perform post-processing.

  • The chain continues until a response is generated.

Each middleware can:

  • Process the incoming request.

  • Pass control to the next middleware using await next().

  • Short-circuit the chain by returning a response.

  • Modify the context object shared across all handlers.

What is Middleware For?

Middleware enables separation of concerns by handling cross-cutting functionality like:

  • Authentication

    • Basic Auth: Validates username/password credentials.

    • Bearer Auth: Validates bearer tokens.

    • JWT: Verifies JSON Web Tokens and stores payload in context.

  • HTTP Utilities

    • CORS: Handles Cross-Origin Resource Sharing headers.

    • ETag: Generates ETags and returns 304 responses for unchanged content.

    • Cache: Implements HTTP caching using the Cache API.

  • Security

    • CSRF: Protects against Cross-Site Request Forgery attacks.
  • Request Processing

    • Validation: Validates request data (JSON, form, query, params).

How to Use Middlewares?

A middleware can be applied globally to all routes, to specific path patterns, directly to individual routes, chained together, or through sub-app routing. Each method provides different levels of scope and control over request processing.

Global Application

Apply middleware to all routes using the wildcard pattern:

app.use('*', async (c, next) => {  
  console.log(`${c.req.method} : ${c.req.url}`)  
  await next()  
})

Path-Specific Application

Apply middleware to routes matching a path pattern:

// Apply to all routes under /api  
app.use('/api/*', cors())  
// Apply to specific route  
app.use('/hello', async (c, next) => {  
  await next()  
  c.res.headers.append('x-message', 'custom-header')  
})

Route-Level Application

Pass middleware directly as arguments to route methods:

// Single middleware with handler  
app.get('/protected', authMiddleware, (c) => {  
  return c.text('Authorized')  
})  
// Multiple middleware chained  
app.get('/api/data',   
  middleware1,  
  middleware2,  
  (c) => c.json({ data: 'success' })  
)

Chained Application

Chain multiple middleware calls for the same path:

app  
  .use('/chained/*', async (c, next) => {  
    c.req.raw.headers.append('x-before', 'abc')  
    await next()  
  })  
  .use(async (c, next) => {  
    await next()  
    c.header('x-after', c.req.header('x-before'))  
  })  
  .get('/chained/abc', (c) => {  
    return c.text('GET chained')  
  })

Sub-Application Routing

Apply middleware through sub-apps using app.route():

const api = new Hono()  
api.use('*', async (c, next) => {  
  await next()  
  c.res.headers.append('x-custom-a', 'a')  
})  

const middleware = new Hono()  
middleware.use('*', async (c, next) => {  
  await next()  
  c.res.headers.append('x-custom-b', 'b')  
})  

app.route('/api', middleware)  
app.route('/api', api)

Hono Validation System

Hono's validation system is built on middleware that intercepts requests, validates specific parts (body, query parameters, headers, path parameters), and either allows the request to proceed or returns validation errors.

hono/validator

hono/validator is a middleware that validates incoming request data across different targets (JSON body, form data, query parameters, path parameters, headers, and cookies) with full TypeScript type safety.

The validator function creates middleware that:

  • Extracts data from a specific request target. The supported targets are:

    • Body Validation (json): Validates JSON request bodies.

    • Query Parameters (query): Validates URL query strings.

    • Path Parameters (param): Validates route parameters.

    • Headers (header): Validates HTTP headers.

    • Form Parameters (form): Validates form data from the request body.

    • Cookies (cookie): Validates HTTP cookies.

  • Runs a validation function on the extracted data.

  • Either returns an error response (short-circuiting) or stores validated data.

  • Makes validated data accessible via c.req.valid(target).

app.get('/search',     
  validator('query', (value, c) => {    
    if (!value.q) {    
      return c.text('Parameter not found', 400)    
    }    
    return value as { q: string }    
  }),    
  (c) => {    
    const { q } = c.req.valid('query')
    return c.text(`Searching ${q}!!!`, 200)    
  }    
)

The validator function receives:

  • value: The extracted data from the target.

  • c: The Context object.

It can return:

  • Validated data: Stored and accessible via c.req.valid().

  • A Response object: Short-circuits the middleware chain with an error response.

  • Throw an error: Propagates through the error handler.

Based on this validation system, the ecosystem provides adapters for multiple validation libraries:

  • @hono/zod-validator: Uses Zod, supports v3/v4.

  • @hono/valibot-validator: Uses Valibot, a modular validation approach.

  • @hono/typebox-validator: Uses TypeBox, JSON Schema compliance.

  • @hono/standard-validator: Uses Standard Schema V1 specification.

Implementing Zod Validations

Let's start by installing the following package:

npm install @hono/zod-validator

The validation starts with defining schemas. In the features/stores/store.ts file, we define the store data structure:

import { z } from 'zod';

export const storeSchema = z.object({
  storeId: z.uuidv7(),
  name: z.string().min(1).max(1024),
  url: z.url().min(1).max(2048),
});

export type Store = z.infer<typeof storeSchema>;

export const stores: Store[] = [];

This schema enforces several validation rules:

  • storeId must be a valid UUIDv7.

  • name must be a non-empty string with a maximum length of 1024 characters.

  • url must be a valid URL with a maximum length of 2048 characters.

The Store type is automatically inferred from the schema, ensuring TypeScript types and runtime validation stay synchronized. The pagination schema in the types/pagination.ts file demonstrates Zod's advanced features:

import { z } from 'zod';

const DEFAULT_PAGE_NUMBER = 1;
const DEFAULT_PAGE_SIZE = 10;
const MAX_PAGE_SIZE = 100;

export const paginationSchema = z.object({
  pageNumber: z.coerce.number().min(1).optional().default(DEFAULT_PAGE_NUMBER),
  pageSize: z.coerce
    .number()
    .min(1)
    .max(MAX_PAGE_SIZE)
    .optional()
    .default(DEFAULT_PAGE_SIZE),
});

export const createPage = <TResult>(
  items: TResult[],
  totalCount: number,
  pageNumber: number,
  pageSize: number
) => {
  const totalPages = Math.ceil(totalCount / pageSize);
  return {
    items,
    pageNumber,
    pageSize,
    totalPages,
    totalCount,
  };
};

The z.coerce.number() method automatically converts string query parameters to numbers, which is essential since HTTP query parameters are always strings. The features/stores/add-store.ts file demonstrates body validation:

import { Hono } from 'hono';
import { v7 } from 'uuid';
import { StatusCodes } from 'http-status-codes';
import { stores, storeSchema } from './store.js';
import { zValidator } from '@hono/zod-validator';

const schema = storeSchema.omit({ storeId: true });

export const addRoute = new Hono().post(
  '/',
  zValidator('json', schema),
  async c => {
    const { name, url } = c.req.valid('json');
    const store = { name, url, storeId: v7() };
    stores.push(store);
    return c.json(store, StatusCodes.CREATED);
  }
);
  • Uses storeSchema.omit({ storeId: true }) to generate a new schema excluding the storeId property since it's generated server-side.

  • Validates the JSON request body with the zValidator('json', schema) middleware.

  • Provides type-safe access to name and url from the validated body.

For retrieving a specific store in the features/stores/get-store.ts file, we validate the path parameter:

import { Hono } from 'hono';
import { stores, storeSchema } from './store.js';
import { StatusCodes } from 'http-status-codes';
import { zValidator } from '@hono/zod-validator';

const schema = storeSchema.pick({ storeId: true });

export const getRoute = new Hono().get(
  '/:storeId',
  zValidator('param', schema),
  async c => {
    const { storeId } = c.req.valid('param');
    const store = stores.find(s => s.storeId === storeId);
    if (!store) {
      return c.json(
        { message: `Store ${storeId} not found` },
        StatusCodes.NOT_FOUND
      );
    }
    return c.json(store, StatusCodes.OK);
  }
);
  • storeSchema.pick({ storeId: true }) creates a new schema with only the storeId field.

  • This ensures the URL parameter is a valid UUIDv7.

  • The validated storeId is type-safe and guaranteed to match the schema.

In the features/stores/list-stores.ts file, we validate query parameters for listing stores:

import { Hono } from 'hono';
import { stores } from './store.js';
import { StatusCodes } from 'http-status-codes';
import { paginationSchema, createPage } from '@/types/pagination.js';
import { z } from 'zod';
import { zValidator } from '@hono/zod-validator';

const schema = paginationSchema.extend({
  name: z.string().optional(),
});

export const listRoute = new Hono().get(
  '/',
  zValidator('query', schema),
  async c => {
    const { pageNumber, pageSize, name } = c.req.valid('query');
    const pn = pageNumber;
    const pz = pageSize;
    let filteredStores = stores;

    if (name) {
      filteredStores = stores.filter(store =>
        store.name.toLowerCase().includes(name.toLowerCase())
      );
    }
    const totalCount = filteredStores.length;
    const startIndex = (pn - 1) * pz;
    const endIndex = startIndex + pz;
    const page = filteredStores.slice(startIndex, endIndex);
    return c.json(createPage(page, totalCount, pn, pz), StatusCodes.OK);
  }
);
  • We extend the paginationSchema with an optional name filter.

  • The zValidator('query', schema) middleware validates query parameters before the handler executes.

  • c.req.valid('query') provides type-safe access to validated query parameters.

  • If validation fails, a 400 error is returned automatically.

The edit store endpoint in the features/stores/edit-store.ts file shows how to validate request parts:

import { Hono } from 'hono';
import { StatusCodes } from 'http-status-codes';
import { stores, storeSchema } from './store.js';
import { zValidator } from '@hono/zod-validator';

const paramSchema = storeSchema.pick({ storeId: true });
const bodySchema = storeSchema.pick({ name: true, url: true });

export const editRoute = new Hono().put(
  '/:storeId',
  zValidator('param', paramSchema),
  zValidator('json', bodySchema),
  async c => {
    const { storeId } = c.req.valid('param');
    const { name, url } = c.req.valid('json');
    const store = stores.find(s => s.storeId === storeId);
    if (!store) {
      return c.json(
        { message: `Store ${storeId} not found` },
        StatusCodes.NOT_FOUND
      );
    }
    store.name = name;
    store.url = url;
    return c.json(store, StatusCodes.OK);
  }
);

Multiple validators can be chained, each validating a different part of the request. The order matters: validators execute in the order they're declared.

The combination of Hono's middleware system and Zod's type-safe validation provides a powerful foundation for building robust APIs with minimal boilerplate. By mastering validation patterns, we'll build APIs that are more secure, maintainable, and easier to work with for both our team and API consumers. You can find all the code here. Thanks, and happy coding.

More from this blog

raulnq

171 posts

Somebody who likes to code