<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ hono - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ hono - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 22 Sep 2026 11:54:26 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/hono/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build Type-Safe APIs with Hono and Zod ]]>
                </title>
                <description>
                    <![CDATA[ If you've shipped a Node.js API before, you already know this pain: your TypeScript types say one thing, your runtime validation says another, and your OpenAPI docs quietly disagree with both. Someone ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-type-safe-apis-with-hono-and-zod/</link>
                <guid isPermaLink="false">6a8c4f18dbeb4eb3a4b2ed0d</guid>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ hono ]]>
                    </category>
                
                    <category>
                        <![CDATA[ zod ]]>
                    </category>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ webdev ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chinedu Otutu ]]>
                </dc:creator>
                <pubDate>Mon, 24 Aug 2026 14:03:04 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/ce251811-43ad-4e2f-9761-83b6240f718e.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you've shipped a Node.js API before, you already know this pain: your TypeScript types say one thing, your runtime validation says another, and your OpenAPI docs quietly disagree with both.</p>
<p>Someone adds a field to an interface, and the schema never gets updated. The docs stay stale until a client files a bug. There's no compiler error or failing test. The three sources of truth just drift apart.</p>
<p>In this tutorial, you'll learn how to collapse those three concerns into one definition using <a href="https://hono.dev">Hono</a> and <a href="https://zod.dev">Zod</a>. You'll build the same patterns I use in production, including in <a href="https://github.com/otutukingsley/clipforge">ClipForge</a>, an open-source video processing toolkit I built and maintain. They help validation, types, and docs stay in sync by design.</p>
<p>By the end of this article, you should know how to:</p>
<ul>
<li><p>Define one Zod schema that handles runtime validation, TypeScript types, and OpenAPI docs</p>
</li>
<li><p>Structure routes so the contract and the handler stay separated</p>
</li>
<li><p>Keep database schemas and HTTP schemas as two deliberate layers</p>
</li>
<li><p>Return one consistent error shape from every failure path</p>
</li>
<li><p>Carry those same patterns from a small Tasks API into a real multi-service system</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To get the most out of this article, you'll need to know:</p>
<ul>
<li><p>JavaScript and basic TypeScript</p>
</li>
<li><p>How REST APIs work (routes, request bodies, status codes)</p>
</li>
<li><p>A little Node.js (installing packages, running scripts)</p>
</li>
</ul>
<p>You don't need prior experience with Hono, Zod, or Drizzle.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-1-the-drift-problem">1. The Drift Problem</a></p>
</li>
<li><p><a href="#heading-2-what-is-hono">2. What Is Hono?</a></p>
</li>
<li><p><a href="#heading-3-what-is-zod">3. What Is Zod?</a></p>
</li>
<li><p><a href="#heading-4-one-schema-three-jobs">4. One Schema, Three Jobs</a></p>
</li>
<li><p><a href="#heading-5-how-to-set-up-the-project">5. How to Set Up the Project</a></p>
</li>
<li><p><a href="#heading-6-how-to-define-your-api-schemas">6. How to Define Your API Schemas</a></p>
</li>
<li><p><a href="#heading-7-how-to-separate-database-schemas-from-api-schemas">7. How to Separate Database Schemas from API Schemas</a></p>
</li>
<li><p><a href="#heading-8-how-to-define-routes-as-contracts">8. How to Define Routes as Contracts</a></p>
</li>
<li><p><a href="#heading-9-how-to-keep-handlers-thin">9. How to Keep Handlers Thin</a></p>
</li>
<li><p><a href="#heading-10-how-to-return-one-error-shape-everywhere">10. How to Return One Error Shape Everywhere</a></p>
</li>
<li><p><a href="#heading-11-how-to-generate-docs-that-cant-drift">11. How to Generate Docs That Can't Drift</a></p>
</li>
<li><p><a href="#heading-12-how-to-make-the-app-production-ready">12. How to Make the App Production-Ready</a></p>
</li>
<li><p><a href="#heading-13-how-these-patterns-scale-in-a-production-app">13. How These Patterns Scale in a Production App</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-1-the-drift-problem">1. The Drift Problem</h2>
<p>Most TypeScript APIs end up maintaining three separate descriptions of the same data:</p>
<ol>
<li><p><strong>Runtime validation</strong> checks that run when a request arrives</p>
</li>
<li><p><strong>TypeScript types</strong> <strong>shapes</strong> the compiler understands at build time</p>
</li>
<li><p><strong>API documentation</strong>, the contract you show to consumers</p>
</li>
</ol>
<p>Each one lives in a different file and updates on a different schedule. And none of them can see the others.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/1b34f0ca-6b0f-48c0-9282-7bc07d99f171.png" alt="Three API sources of truth often drifting out of sync: TypeScript types, runtime validation, and OpenAPI docs" style="display: block;" width="1100" height="520" loading="lazy">

<p><em>Figure 1: Three descriptions of the same data, maintained separately and often out of sync.</em></p>
<p>Hand-written interfaces disappear at runtime. Joi or Yup schemas validate data but don't give you types for free. OpenAPI files are usually edited by hand, if they're edited at all.</p>
<p>The fix is not "be more careful." The fix is one definition that produces all three outputs.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/df2d559a-6afb-469d-81cb-363ab54d0dba.png" alt="One Zod schema producing runtime validation, TypeScript types, and OpenAPI docs" style="display: block;" width="1100" height="480" loading="lazy">

<p><em>Figure 2: One schema definition produces three outputs.</em></p>
<p>That is what Hono and Zod give you when you use them together through <code>@hono/zod-openapi</code>.</p>
<h2 id="heading-2-what-is-hono">2. What Is Hono?</h2>
<p><a href="https://hono.dev">Hono</a> is a small, fast web framework built on Web Standard APIs, the same <code>Request</code> and <code>Response</code> primitives that run in Node.js, Deno, Bun, and Cloudflare Workers.</p>
<p>Compared with Express, that difference matters:</p>
<table>
<thead>
<tr>
<th>Express</th>
<th>Hono</th>
</tr>
</thead>
<tbody><tr>
<td>Node-specific <code>req</code> / <code>res</code></td>
<td>Web Standard APIs</td>
</tr>
<tr>
<td>Params are untyped strings</td>
<td>Params validated and typed with Zod</td>
</tr>
<tr>
<td>Validation is your problem</td>
<td>Route definition is the OpenAPI entry</td>
</tr>
<tr>
<td>Runs on Node only</td>
<td>Runs on Node, Bun, Deno, and the edge</td>
</tr>
</tbody></table>
<p>Hono's core is about 14kb. On Node it's roughly 5–7× faster than Express for the same workload. On Bun or Cloudflare Workers, the gap widens further because those runtimes are optimized for web standards.</p>
<p>For most CRUD APIs, your database is still the bottleneck. But at high concurrency, or on edge runtimes where cold starts matter, the framework gap is real.</p>
<p>More importantly for this tutorial: Hono's OpenAPI integration lets your route definition <em>be</em> the documentation.</p>
<h2 id="heading-3-what-is-zod">3. What Is Zod?</h2>
<p><a href="https://zod.dev">Zod</a> is a TypeScript-first schema validation library. You describe the shape of your data once. Zod then:</p>
<ol>
<li><p>Validates that shape at runtime</p>
</li>
<li><p>Infers the TypeScript type with <code>z.infer</code></p>
</li>
<li><p>Feeds OpenAPI docs when you attach <code>.openapi()</code> metadata</p>
</li>
</ol>
<p>With Joi or Yup, you usually validate at runtime and then hand-write a matching interface. That's two definitions again. Zod removes the second one.</p>
<pre><code class="language-typescript">import { z } from 'zod';

const createTaskSchema = z.object({
  title: z.string().min(1).max(120),
  status: z.enum(['todo', 'in_progress', 'done']).default('todo'),
});

type CreateTaskInput = z.infer&lt;typeof createTaskSchema&gt;;
// { title: string; status?: "todo" | "in_progress" | "done" }
</code></pre>
<p>Change the schema, and every callsite that depends on <code>CreateTaskInput</code> updates with it. TypeScript will tell you what broke.</p>
<h2 id="heading-4-one-schema-three-jobs">4. One Schema, Three Jobs</h2>
<p>Here's the mental model for the rest of the article:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/890719d0-6ab0-4529-ad07-565a7db9e604.png" alt="taskSchema feeding runtime validation, TypeScript types, and OpenAPI documentation" style="display: block;" width="1200" height="500" loading="lazy">

<p><em>Figure 3:</em> <code>taskSchema</code> <em>is the single source of truth for validation, types, and docs.</em></p>
<ol>
<li><p><strong>Runtime validation:</strong> bad data is rejected before it reaches your handler, with structured field errors instead of a stack trace</p>
</li>
<li><p><strong>TypeScript types:</strong> <code>z.infer&lt;typeof schema&gt;</code> is derived from the schema, not maintained beside it</p>
</li>
<li><p><strong>OpenAPI docs:</strong> <code>.openapi('Name')</code> registers the schema in the generated spec, so <code>/reference</code> stays current</p>
</li>
</ol>
<p>One definition with three outputs. Nothing to keep in sync by hand.</p>
<h2 id="heading-5-how-to-set-up-the-project">5. How to Set Up the Project</h2>
<p>We'll use the companion demo from <a href="https://github.com/otutukingsley/api-conf-demo">api-conf-demo</a>. It's a small Tasks API that shows the patterns cleanly. Later, we'll look at how the same ideas show up in ClipForge at a larger scale.</p>
<p>Clone the repo and install dependencies:</p>
<pre><code class="language-bash">git clone https://github.com/otutukingsley/api-conf-demo.git
cd api-conf-demo
npm install
</code></pre>
<p>Start the server:</p>
<pre><code class="language-bash">npm run dev
</code></pre>
<p>You should see the API on <code>http://localhost:8080</code>, with interactive docs at <code>/reference</code>.</p>
<p>The important folders look like this:</p>
<pre><code class="language-text">src/
├── db/schema/          # Persistence layer (Drizzle tables)
├── lib/schemas/        # HTTP contract layer (Zod + OpenAPI)
├── lib/errors/         # One error envelope for every failure
├── routes/tasks/       # Route contracts + handlers
├── services/           # Business logic and DB access
├── app.ts              # Middleware, routers, OpenAPI wiring
└── env.ts              # Zod-validated environment config
</code></pre>
<p>This is still MVC. The tooling is just better:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/bd701dc8-6f83-490a-905d-fbba09438154.png" alt="MVC-style layers with HTTP client, controller, contract schemas, service model, and database" style="display: block;" width="900" height="720" loading="lazy">

<p><em>Figure 4: Still MVC. Routes and handlers as the controller, schemas as the contract, and services as the model.</em></p>
<ul>
<li><p><strong>Model</strong> services its own business logic and database access</p>
</li>
<li><p><strong>View / contract</strong> schemas define what data looks like at the DB and HTTP boundaries</p>
</li>
<li><p><strong>Controller</strong> routes declare the contract, handlers fulfill it</p>
</li>
</ul>
<p>A request through the demo API looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/5019c39c-8c45-46f4-8e68-b6388e4c9944.png" alt="Sequence diagram of a POST /tasks request through middleware, route contract, handler, service, and database" style="display: block;" width="1300" height="620" loading="lazy">

<p><em>Figure 5: Request flow through the demo API, including the validation failure path.</em></p>
<p>In that diagram, a request flows through the Client, Middleware, Route contract, Handler, Service, and DB. Middleware handles logging and CORS. The route contract validates the body with Zod.</p>
<p>If validation fails, the client gets a <code>422 ApiError</code> and the handler never runs. If it passes, the handler gets a typed <code>CreateTaskInput</code>, calls <code>TaskService.create()</code>, the service inserts and parses the row, and the client gets <code>201</code> with the Task JSON.</p>
<h2 id="heading-6-how-to-define-your-api-schemas">6. How to Define Your API Schemas</h2>
<p>Start with the HTTP contract in <code>src/lib/schemas/task.ts</code>:</p>
<pre><code class="language-typescript">import { z } from '@hono/zod-openapi';

export const taskStatusSchema = z
  .enum(['todo', 'in_progress', 'done'])
  .openapi('TaskStatus');

export const taskSchema = z
  .object({
    id: z.string().uuid().openapi({
      example: '8e2c9f0a-2222-4a5a-9c3e-1a2b3c4d5e6f',
    }),
    title: z.string().min(1).max(120).openapi({
      example: 'Write the talk abstract',
    }),
    description: z.string().max(2000).nullable().openapi({
      example: 'Cover Hono + Zod patterns',
    }),
    status: taskStatusSchema.default('todo'),
    dueDate: z.string().date().nullable().openapi({
      example: '2026-07-15',
    }),
    createdAt: z.string().openapi({ example: '2026-06-28 10:15:00' }),
    updatedAt: z.string().openapi({ example: '2026-06-28 10:15:00' }),
  })
  .openapi('Task');

export type Task = z.infer&lt;typeof taskSchema&gt;;
</code></pre>
<p>That one object is now the runtime validator, TypeScript type, and an OpenAPI component named <code>Task</code>.</p>
<p>Request schemas should derive from the same base instead of being redeclared:</p>
<pre><code class="language-typescript">export const createTaskSchema = taskSchema
  .pick({ title: true, description: true, status: true, dueDate: true })
  .partial({ description: true, status: true, dueDate: true })
  .openapi('CreateTask');

export const updateTaskSchema = createTaskSchema
  .partial()
  .openapi('UpdateTask');

export type CreateTaskInput = z.infer&lt;typeof createTaskSchema&gt;;
export type UpdateTaskInput = z.infer&lt;typeof updateTaskSchema&gt;;
</code></pre>
<p><code>CreateTask</code> never includes <code>id</code>, because clients don't send one. <code>UpdateTask</code> makes every field optional, because a <code>PATCH</code> can touch any subset.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/2f7cd7b3-1f40-4102-83f9-2c682eecc301.png" alt="taskSchema deriving createTaskSchema and updateTaskSchema" style="display: block;" width="780" height="620" loading="lazy">

<p><em>Figure 6: Request schemas derive from the same base resource schema.</em></p>
<p>We have three contracts with one source.</p>
<h2 id="heading-7-how-to-separate-database-schemas-from-api-schemas">7. How to Separate Database Schemas from API Schemas</h2>
<p>It's tempting to treat the database row and the API response as the same shape. In a tiny demo, they often look identical. In production, they diverge.</p>
<p>Keep them in separate files on purpose:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/7a80d075-4f35-467a-8470-3d87448a5982.png" alt="Database persistence schemas mapped to HTTP contract schemas in the service layer" style="display: block;" width="1200" height="520" loading="lazy">

<p><em>Figure 7: Keep database schemas and HTTP schemas as two deliberate layers.</em></p>
<p>Figure 7 has two panels connected by an arrow labeled "map" in the service.</p>
<p>Left pane <code>src/db/schema/</code> (persistence):</p>
<ul>
<li><p>Starts from a Drizzle table definition</p>
</li>
<li><p>That table drives SQL migrations</p>
</li>
<li><p>It also produces drizzle-zod schemas for parsing rows</p>
</li>
<li><p>And row insert/select types for TypeScript at the database boundary</p>
</li>
</ul>
<p>Right panel <code>src/lib/schemas/</code> (HTTP contract):</p>
<ul>
<li><p>Uses Zod + <code>.openapi()</code> as the public contract</p>
</li>
<li><p>Defines request bodies / params clients may send</p>
</li>
<li><p>Defines response bodies clients receive</p>
</li>
<li><p>Registers OpenAPI components used by <code>/doc</code> and <code>/reference</code></p>
</li>
</ul>
<p>The arrow in the middle matters: the database shape isn't automatically the API shape. The <strong>service</strong> maps between them. That's why an internal column can exist on the left without becoming part of the HTTP contract on the right.</p>
<p>In short:</p>
<ul>
<li><p><code>src/db/schema/</code>: what a row looks like in the database</p>
</li>
<li><p><code>src/lib/schemas/</code>: what the HTTP contract looks like</p>
</li>
</ul>
<p>Here's the Drizzle table from the demo:</p>
<pre><code class="language-typescript">import { sql } from 'drizzle-orm';
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
import { z } from 'zod';

export const taskStatusValues = ['todo', 'in_progress', 'done'] as const;

export const tasks = sqliteTable('tasks', {
  id: text('id').primaryKey(),
  title: text('title').notNull(),
  description: text('description'),
  status: text('status', { enum: taskStatusValues }).notNull().default('todo'),
  dueDate: text('due_date'),
  createdAt: text('created_at')
    .notNull()
    .default(sql`(current_timestamp)`),
  updatedAt: text('updated_at')
    .notNull()
    .default(sql`(current_timestamp)`),
});

export const selectTaskSchema = createSelectSchema(tasks, {
  status: z.enum(taskStatusValues),
});

export const insertTaskSchema = createInsertSchema(tasks, {
  id: () =&gt; z.string().uuid().optional(),
  title: () =&gt; z.string().min(1).max(120),
  description: () =&gt; z.string().max(2000).nullable().optional(),
  status: z.enum(taskStatusValues).optional(),
  dueDate: () =&gt; z.string().date().nullable().optional(),
});
</code></pre>
<p>One table definition gives you three outputs:</p>
<ol>
<li><p><strong>TypeScript types</strong> inferred from the columns</p>
</li>
<li><p><strong>SQL migrations</strong> generated with <code>drizzle-kit</code></p>
</li>
<li><p><strong>Zod schemas</strong> via <code>drizzle-zod</code></p>
</li>
</ol>
<p>That's useful at the database boundary. It's not a replacement for your HTTP schemas.</p>
<p>The moment you add an internal <code>archivedAt</code> column, or a computed field the API returns that's not a column, the split pays for itself. Divergence becomes a normal change instead of a painful refactor.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/b055d171-05a7-4661-8a52-73db3f28c8b5.png" alt="Service mapping a database row with internal fields to a public API response" style="display: block;" width="1100" height="320" loading="lazy">

<p><em>Figure 8: The service decides what the client may see.</em></p>
<h2 id="heading-8-how-to-define-routes-as-contracts">8. How to Define Routes as Contracts</h2>
<p>In this architecture, a route doesn't "just handle a request." A route declares the contract: method, path, request schemas, and response schemas.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/b8a19630-7447-4a9e-a643-3c950d7a88cc.png" alt="Route definition acting as the API contract fulfilled by a handler" style="display: block;" width="900" height="560" loading="lazy">

<p><em>Figure 9: The route declares the contract. The handler fulfills it.</em></p>
<pre><code class="language-typescript">import { createRoute, z } from '@hono/zod-openapi';
import * as HttpStatusCodes from '@/lib/http-status-codes';
import { jsonContent } from '@/lib/openapi/json-content';
import { jsonApiErrorContent } from '@/lib/openapi/error-schema';
import {
  createTaskSchema,
  taskParamsSchema,
  taskSchema,
} from '@/lib/schemas/task';

export const createTask = createRoute({
  tags: ['Tasks'],
  method: 'post',
  path: '/tasks',
  summary: 'Create a task',
  request: {
    body: jsonContent(createTaskSchema, 'The task to create'),
  },
  responses: {
    [HttpStatusCodes.CREATED]: jsonContent(taskSchema, 'The created task'),
    [HttpStatusCodes.UNPROCESSABLE_ENTITY]:
      jsonApiErrorContent('Validation error'),
    [HttpStatusCodes.INTERNAL_SERVER_ERROR]: jsonApiErrorContent(
      'Internal server error',
    ),
  },
});

export const getTask = createRoute({
  tags: ['Tasks'],
  method: 'get',
  path: '/tasks/{id}',
  summary: 'Get a task by ID',
  request: {
    params: taskParamsSchema,
  },
  responses: {
    [HttpStatusCodes.OK]: jsonContent(taskSchema, 'The requested task'),
    [HttpStatusCodes.NOT_FOUND]: jsonApiErrorContent('Task not found'),
    [HttpStatusCodes.UNPROCESSABLE_ENTITY]:
      jsonApiErrorContent('Validation error'),
    [HttpStatusCodes.INTERNAL_SERVER_ERROR]: jsonApiErrorContent(
      'Internal server error',
    ),
  },
});
</code></pre>
<p>A small helper keeps response boilerplate readable:</p>
<pre><code class="language-typescript">export function jsonContent&lt;T extends z.ZodTypeAny&gt;(
  schema: T,
  description: string,
) {
  return {
    content: {
      'application/json': { schema },
    },
    description,
  };
}
</code></pre>
<p>Named status constants replace magic numbers. You use the same constants as object keys in the route and as switch cases in the handler. That makes status codes searchable and consistent.</p>
<h2 id="heading-9-how-to-keep-handlers-thin">9. How to Keep Handlers Thin</h2>
<p>Once the route defines the contract, the handler only has to fulfill it.</p>
<pre><code class="language-typescript">export const getTask: AppRouteHandler&lt;GetTaskRoute&gt; = (c) =&gt; {
  try {
    const { id } = c.req.valid('param');
    return c.json(TaskService.get(id), HttpStatusCodes.OK);
  } catch (error) {
    const apiError = ApiError.parse(error);
    switch (apiError.statusCode) {
      case HttpStatusCodes.NOT_FOUND:
      case HttpStatusCodes.UNPROCESSABLE_ENTITY:
        return c.json(apiError.toResponseBody(), apiError.statusCode);
      default:
        return c.json(
          apiError.toResponseBody(),
          HttpStatusCodes.INTERNAL_SERVER_ERROR,
        );
    }
  }
};

export const createTask: AppRouteHandler&lt;CreateTaskRoute&gt; = (c) =&gt; {
  try {
    const body = c.req.valid('json');
    return c.json(TaskService.create(body), HttpStatusCodes.CREATED);
  } catch (error) {
    const apiError = ApiError.parse(error);
    switch (apiError.statusCode) {
      case HttpStatusCodes.UNPROCESSABLE_ENTITY:
        return c.json(apiError.toResponseBody(), apiError.statusCode);
      default:
        return c.json(
          apiError.toResponseBody(),
          HttpStatusCodes.INTERNAL_SERVER_ERROR,
        );
    }
  }
};
</code></pre>
<p>Notice what's <em>not</em> in the handler:</p>
<ul>
<li><p>No manual parsing of params or bodies</p>
</li>
<li><p>No casting to <code>any</code></p>
</li>
<li><p>No business logic</p>
</li>
</ul>
<p><code>c.req.valid('param')</code> and <code>c.req.valid('json')</code> are already validated and typed. The service owns the database work:</p>
<pre><code class="language-typescript">get(id: string): Task {
  try {
    const row = db.select().from(tasks).where(eq(tasks.id, id)).get();
    if (!row) throw new NotFoundError(`Task ${id} not found`);
    return selectTaskSchema.parse(row);
  } catch (error) {
    throw ApiError.parse(error);
  }
},
</code></pre>
<p>The service wraps its body in one <code>try/catch</code> and normalizes every failure through <code>ApiError.parse()</code>. Zod errors, custom domain errors, and unexpected driver errors all become one typed shape.</p>
<h2 id="heading-10-how-to-return-one-error-shape-everywhere">10. How to Return One Error Shape Everywhere</h2>
<p>Clients should never guess whether an error looks like <code>{ message }</code>, <code>{ error }</code>, or a raw stack trace.</p>
<p>In the demo, every failure becomes one envelope:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/85080f92-0fde-45cf-9b2e-eeb4f7ec5ef5.png" alt="Different error sources normalized through ApiError.parse into one JSON envelope" style="display: block;" width="1100" height="560" loading="lazy">

<p><em>Figure 10: Every failure path becomes one predictable error shape.</em></p>
<p>The factory that creates routers bakes that behavior in with <code>defaultHook</code>:</p>
<pre><code class="language-typescript">export function createRouter() {
  return new OpenAPIHono({
    defaultHook: (result, c) =&gt; {
      if (!result.success) {
        const apiError = ApiError.parse(result.error);
        return c.json(apiError.toResponseBody(), apiError.statusCode);
      }
    },
  });
}
</code></pre>
<p>Every router goes through that factory. A bad UUID in a path param, a missing field in a POST body, or an invalid enum in a query string all produce the same response shape before the handler runs.</p>
<p><code>ApiError.parse()</code> is the second half of the pattern:</p>
<pre><code class="language-typescript">public static parse(error: unknown): ApiError {
  if (error instanceof ApiError) return error;

  if (error instanceof ZodError) {
    return new ApiError('Validation error', {
      statusCode: 422,
      errors: error.flatten().fieldErrors,
    });
  }

  return new ApiError('Internal server error', { statusCode: 500 });
}
</code></pre>
<p>Handlers still use an explicit <code>switch</code> on status codes. That's intentional. Each route documents exactly which statuses it can return. A handler that should never emit <code>403</code> doesn't have <code>403</code> sitting in a shared helper's defaults.</p>
<p>The repetition is the point. Explicit beats clever here.</p>
<h2 id="heading-11-how-to-generate-docs-that-cant-drift">11. How to Generate Docs That Can't Drift</h2>
<p>Because schemas are attached to route definitions, OpenAPI becomes a byproduct of the code instead of a separate chore.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/aac24756-84db-48f2-936a-3370c36b3dd2.png" alt="Zod schemas flowing into route definitions, OpenAPI JSON, and Scalar reference UI" style="display: block;" width="1200" height="280" loading="lazy">

<p><em>Figure 11: Docs are generated from the same route definitions as the runtime code.</em></p>
<pre><code class="language-typescript">export function configureOpenAPI(app: OpenAPIHono) {
  app.doc('/doc', {
    openapi: '3.0.0',
    info: {
      title: 'Bulletproof Tasks API',
      version: '1.0.0',
    },
  });

  app.get(
    '/reference',
    apiReference({
      spec: { url: '/doc' },
      theme: 'kepler',
      layout: 'modern',
      pageTitle: 'Bulletproof Tasks API',
    }),
  );
}
</code></pre>
<ul>
<li><p><code>GET /doc</code> returns the raw OpenAPI JSON</p>
</li>
<li><p><code>GET /reference</code> serves an interactive Scalar explorer</p>
</li>
</ul>
<p>When you change a schema or a response code in the route file, the docs update with it. There's no second docs step to forget.</p>
<h2 id="heading-12-how-to-make-the-app-production-ready">12. How to Make the App Production-Ready</h2>
<p>Type safety at the request boundary isn't enough. Configuration and process lifecycle need the same discipline.</p>
<h3 id="heading-validate-environment-variables-at-boot">Validate Environment Variables at Boot</h3>
<pre><code class="language-typescript">import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z
    .enum(['development', 'test', 'production'])
    .default('development'),
  LOG_LEVEL: z
    .enum(['silent', 'debug', 'info', 'warn', 'error', 'fatal'])
    .default('info'),
  PORT: z.coerce.number().default(8080),
  DATABASE_URL: z.string().default('tasks.db'),
});

export const env = envSchema.parse(process.env);
</code></pre>
<p>If a required value is missing or malformed, the process exits immediately with a clear Zod error. That's much better than discovering <code>undefined</code> three requests into production.</p>
<h3 id="heading-prefer-structured-logging">Prefer Structured Logging</h3>
<p>The demo uses Pino through <code>hono-pino</code> instead of a one-line console logger. In production you want JSON logs. In development you want something readable. One middleware can do both, and every request can carry a UUID.</p>
<h3 id="heading-shut-down-cleanly">Shut Down Cleanly</h3>
<p>Docker and process managers send <code>SIGTERM</code> before they kill a process. Handle it. Close the HTTP server, then close the database handle, then exit. Without that, a SQLite file (or a Postgres connection pool) can be left in a dirty state.</p>
<h3 id="heading-keep-the-app-runtime-portable">Keep the App Runtime-Portable</h3>
<p>Hono only depends on Web Standard APIs inside <code>app</code>. That means the same application object can run on Node:</p>
<pre><code class="language-typescript">import { serve } from '@hono/node-server';
import { app } from '@/app';

serve({ fetch: app.fetch, port: env.PORT });
</code></pre>
<p>Or on an edge runtime with almost nothing else:</p>
<pre><code class="language-typescript">import { app } from '@/app';

export default app;
</code></pre>
<p>That;s not a simplified example. That's the whole adapter.</p>
<h2 id="heading-13-how-these-patterns-scale-in-a-production-app">13. How These Patterns Scale in a Production App</h2>
<p>A Tasks API is a good teaching surface. Production systems are messier: you have uploads, background jobs, multiple packages, and longer-lived workflows.</p>
<p><a href="https://github.com/otutukingsley/clipforge">ClipForge</a> is a helpful example of a production app where those same ideas show up at scale. It's a self-hostable video processing toolkit built with Node.js, Hono, Zod, Drizzle, BullMQ, and Nuxt. You upload a video and get transcription, subtitles, summaries, chapter markers, and thumbnails.</p>
<p>The architecture looks like this:</p>
<pre><code class="language-text">apps/
├── api/        # Hono REST API
├── worker/     # BullMQ video processor
└── web/        # Nuxt frontend
packages/
├── shared/     # Zod schemas, constants, shared types
└── providers/  # OpenAI, Anthropic, Deepgram integrations
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/f341110f-ef93-4b79-b0d0-9b07aeed04d2.png" alt="ClipForge architecture with web, API, worker, shared schemas, Postgres, Redis, and AI providers" style="display: block;" width="1200" height="700" loading="lazy">

<p><em>Figure 12: ClipForge keeps the same contract-first core across web, API, and worker packages.</em></p>
<p>A video job moves through stages like uploading, validating, extracting audio, and so on without inventing new response shapes along the way:</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/38487b12-26fd-4185-9ca1-48560a03699e.png" alt="ClipForge video job stages from uploading through complete, with a failed path" style="display: block;" width="1400" height="360" loading="lazy">

<p><em>Figure 13: A video job moves through typed stages without inventing new response shapes.</em></p>
<p>The important part isn't the video pipeline. The important part is that the API still follows the same contract-first rules.</p>
<h3 id="heading-shared-zod-schemas-across-packages">Shared Zod Schemas Across Packages</h3>
<p>ClipForge keeps HTTP contracts in <code>packages/shared</code>, so the API, worker, and web app share one vocabulary:</p>
<pre><code class="language-typescript">export const processingFeatureSchema = z.enum([
  'transcription',
  'subtitles',
  'summary',
  'chapters',
  'thumbnails',
]);

export const videoUploadSchema = z.object({
  title: z.string().min(3).max(200),
  description: z.string().max(2000).optional(),
  features: z
    .array(processingFeatureSchema)
    .default([
      'transcription',
      'subtitles',
      'summary',
      'chapters',
      'thumbnails',
    ]),
  priority: z.enum(['low', 'normal', 'high']).default('normal'),
});

export const jobStatusSchema = z.object({
  jobId: z.string(),
  title: z.string().optional(),
  state: jobStateSchema,
  stage: jobStageSchema,
  progress: z.number().min(0).max(100),
  result: jobResultSchema.optional(),
  failedReason: z.string().optional(),
  createdAt: z.string(),
  updatedAt: z.string(),
});
</code></pre>
<p>When the worker finishes a stage, it doesn't invent a new response shape. It updates state against the same schemas the API returns to the client.</p>
<h3 id="heading-routes-still-define-the-contract">Routes Still Define the Contract</h3>
<p>The upload route in ClipForge looks like the Tasks demo, just with multipart form data and rate limiting:</p>
<pre><code class="language-typescript">export const uploadVideo = createRoute({
  tags: ['videos'],
  method: 'post',
  path: '/videos/upload',
  middleware: [
    sessionMiddleware,
    apiKeysMiddleware,
    rateLimitMiddleware({
      windowMs: 60_000,
      max: 10,
      keyPrefix: 'ratelimit:upload',
    }),
  ] as const,
  request: {
    body: {
      content: {
        'multipart/form-data': {
          schema: z.object({
            file: z.instanceof(File),
            title: z.string().min(3).max(200),
            description: z.string().max(2000).optional(),
            features: z.string().optional(),
            priority: z.enum(['low', 'normal', 'high']).optional(),
          }),
        },
      },
    },
  },
  responses: {
    [HTTP_STATUS.ACCEPTED]: jsonContent(
      jobStatusSchema,
      'Video accepted for processing',
    ),
    [HTTP_STATUS.BAD_REQUEST]: jsonApiErrorContent('Invalid request'),
    [HTTP_STATUS.UNPROCESSABLE]: jsonApiErrorContent('Validation error'),
    [HTTP_STATUS.TOO_MANY_REQUESTS]: jsonApiErrorContent('Rate limit exceeded'),
  },
});
</code></pre>
<p>The handler validates input, writes a job row, enqueues BullMQ work, and returns <code>202 Accepted</code> with a typed job status. The heavy lifting happens in the worker. The API stays a contract layer.</p>
<img src="https://cdn.hashnode.com/uploads/covers/63cd01e7cc7a92b9f77dc1e8/5d3d8157-b1c0-4f40-82f1-9023de847a24.png" alt="Sequence diagram of ClipForge video upload, queue processing, and job status polling" style="display: block;" width="1300" height="680" loading="lazy">

<p><em>Figure 14: The API accepts the upload and returns a typed job status while the worker does the heavy lifting.</em></p>
<p>Figure 14 is a sequence diagram with five participants: Web UI, Hono API, BullMQ, Worker, and Postgres. It shows the async boundary between “accept the upload” and “finish processing.” Here's every step in the diagram:</p>
<ol>
<li><p>The Web UI sends <code>POST /videos/upload</code> to the Hono API.</p>
</li>
<li><p>The Hono API runs <code>Zod + middleware</code> (validate input, session, rate limits, and related checks).</p>
</li>
<li><p>The Hono API performs <code>insert job row</code> in Postgres so the job exists before work starts.</p>
</li>
<li><p>The Hono API sends <code>enqueue process-video</code> to BullMQ.</p>
</li>
<li><p>The Hono API immediately returns <code>202 + jobStatusSchema</code> to the Web UI. At this point the client has a typed job status, but transcription hasn't finished yet.</p>
</li>
<li><p>BullMQ later delivers <code>process job</code> to the Worker.</p>
</li>
<li><p>The Worker runs <code>transcribe / analyze / thumbnails</code> as background work.</p>
</li>
<li><p>The Worker writes <code>update stage + result</code> back to Postgres as stages complete.</p>
</li>
<li><p>The Web UI polls with <code>GET /videos/jobs/{id}</code> against the Hono API.</p>
</li>
<li><p>The Hono API reads the latest job from Postgres and returns <code>jobStatusSchema</code> agai the same response shape as step 5, now with updated <code>stage</code>, <code>progress</code>, and eventually <code>result</code>.</p>
</li>
</ol>
<p>So the API stays a fast contract layer: accept, persist, enqueue, and respond. The worker owns the slow pipeline. The UI tracks progress by polling one share status schema.</p>
<h3 id="heading-database-schemas-still-stay-separate">Database Schemas Still Stay Separate</h3>
<p>ClipForge stores jobs in Postgres with Drizzle:</p>
<pre><code class="language-typescript">export const jobs = pgTable('jobs', {
  id: varchar('id', { length: 36 }).primaryKey(),
  sessionId: varchar('session_id', { length: 36 }).notNull(),
  state: jobStateEnum('state').notNull().default('waiting'),
  stage: jobStageEnum('stage').notNull().default('uploading'),
  progress: integer('progress').notNull().default(0),
  title: varchar('title', { length: 200 }).notNull(),
  features: jsonb('features').$type&lt;string[]&gt;().notNull(),
  provider: varchar('provider', { length: 50 }).notNull().default('openai'),
  result: jsonb('result').$type&lt;JobResult&gt;(),
  failedReason: text('failed_reason'),
  createdAt: timestamp('created_at', { withTimezone: true })
    .notNull()
    .defaultNow(),
  updatedAt: timestamp('updated_at', { withTimezone: true })
    .notNull()
    .defaultNow(),
});
</code></pre>
<p>The table has persistence concerns like <code>filePath</code> and <code>sessionId</code>. The public <code>jobStatusSchema</code> doesn't have to expose all of them. That's the same DB-versus-API split from the Tasks demo, applied to a real workflow.</p>
<h3 id="heading-the-same-production-habits-still-apply">The Same Production Habits Still Apply</h3>
<p>ClipForge validates environment variables with Zod on boot, configures OpenAPI and Scalar at <code>/doc</code> and <code>/reference</code>, normalizes failures through <code>ApiError</code>, and shuts down queues and Redis on <code>SIGTERM</code>.</p>
<p>The lesson is simple: if the small app is structured correctly, the large app doesn't need a different philosophy. It needs more packages, more middleware, and longer-running jobs on top of the same contract-first core.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Type-safe APIs aren't about adding more TypeScript. They're about removing duplicate sources of truth.</p>
<p>With Hono and Zod you can:</p>
<ul>
<li><p>validate requests at runtime</p>
</li>
<li><p>infer types automatically</p>
</li>
<li><p>generate OpenAPI docs from the same route definitions</p>
</li>
<li><p>keep database schemas and HTTP schemas intentionally separate</p>
</li>
<li><p>return one predictable error shape from every failure path</p>
</li>
</ul>
<p>Start with the <a href="https://github.com/otutukingsley/api-conf-demo">api-conf-demo</a> if you want the smallest readable version of these patterns. Then look at <a href="https://github.com/otutukingsley/clipforge">ClipForge</a> to see how the same ideas hold up when the API sits in front of uploads, queues, and multi-stage processing.</p>
<p>Once your route definition is the contract, your docs stop drifting, your handlers get thinner, and your clients get an API they can trust.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Get Type Safety Without Code Generation Using tRPC and Hono ]]>
                </title>
                <description>
                    <![CDATA[ Have you ever updated your backend API property name but neglected to also update the frontend? I'm sure you have. When this occurs, it leads to production crashes and unhappy customers, plus you've wasted your entire week fixing the problem. To reso... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/type-safety-without-code-generation-using-trpc-and-hono/</link>
                <guid isPermaLink="false">696535106ca3442fa7874d4b</guid>
                
                    <category>
                        <![CDATA[ trpc ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ full stack ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ hono ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tarun Singh ]]>
                </dc:creator>
                <pubDate>Mon, 12 Jan 2026 17:53:20 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768240380001/79d5aa1f-438e-4a1e-a072-3166b9a36333.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Have you ever updated your backend API property name but neglected to also update the frontend? I'm sure you have. When this occurs, it leads to production crashes and unhappy customers, plus you've wasted your entire week fixing the problem.</p>
<p>To resolve this issue in the past, you typically had to create a multitude of TypeScript interfaces by hand, using GraphQL Code Generator to generate the interface files, or hope that it all worked out. Well, there’s a better way to accomplish this now, without the need for code generation.</p>
<p><a target="_blank" href="https://trpc.io/">tRPC</a> and <a target="_blank" href="https://hono.dev/">Hono</a> are two applications that are changing how we develop TypeScript-based applications throughout the entirety of the full-stack.</p>
<p>By the end of this tutorial, you’ll understand:</p>
<ul>
<li><p>Why traditional REST APIs fail at type safety</p>
</li>
<li><p>How tRPC provides full end-to-end type inference between backend and frontend</p>
</li>
<li><p>How Hono delivers type-safe APIs while staying REST-friendly</p>
</li>
<li><p>When to choose tRPC vs Hono for your projects</p>
</li>
<li><p>How these tools improve developer experience, team velocity, and reliability</p>
</li>
</ul>
<p>If you’re building full-stack TypeScript applications and want fewer runtime bugs and faster iteration, this guide is for you.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-problem-with-traditional-apis">The Problem with Traditional APIs</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-makes-trpc-different">What Makes tRPC Different?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-hono-the-lightweight-challenger">Hono: The Lightweight Challenger</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-this-matters-these-days">Why This Matters These Days</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-getting-started">Getting Started</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-future-is-type-safe">The Future is Type-Safe</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along comfortably, you should have:</p>
<ul>
<li><p>Basic knowledge of TypeScript</p>
</li>
<li><p>Familiarity with REST APIs and how frontend-backend communication works</p>
</li>
<li><p>Some experience with Node.js and modern JavaScript frameworks</p>
</li>
<li><p>A general understanding of frontend frameworks like React or Next.js (helpful, but not required)</p>
</li>
</ul>
<p>You don’t need prior experience with tRPC, Hono, or GraphQL.</p>
<h2 id="heading-the-problem-with-traditional-apis">The Problem with Traditional APIs</h2>
<p>You’ve probably written something like this a hundred times:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Backend (Express)</span>
app.post(<span class="hljs-string">'/api/users'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> { name, email } = req.body;
  <span class="hljs-comment">// Do stuff with user</span>
  res.json({ id: <span class="hljs-number">1</span>, name, email });
});

<span class="hljs-comment">// Frontend</span>
<span class="hljs-keyword">const</span> createUser = <span class="hljs-keyword">async</span> (name: <span class="hljs-built_in">string</span>, email: <span class="hljs-built_in">string</span>) =&gt; {
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/users'</span>, {
    method: <span class="hljs-string">'POST'</span>,
    headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
    body: <span class="hljs-built_in">JSON</span>.stringify({ name, email })
  });
  <span class="hljs-keyword">return</span> response.json(); 
};
</code></pre>
<p>The backend knows the shape of the data. But the frontend...it hopes it gets it right. You end up writing interfaces manually, like:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">interface</span> User {
  id: <span class="hljs-built_in">number</span>;
  name: <span class="hljs-built_in">string</span>;
  email: <span class="hljs-built_in">string</span>;
}
</code></pre>
<p>If you change the backend tomorrow to return <code>userId</code> instead of <code>id</code>, TypeScript won't catch it. Your types and reality have diverged, and you won't know until runtime.</p>
<p><a target="_blank" href="https://graphql.org/">GraphQL</a> tried to solve this with schemas and codegen, but honestly? Setting up GraphQL feels like assembling IKEA furniture without instructions. You need a schema, resolvers, code generation tools, and suddenly your "simple" API has a 30-minute setup process.</p>
<h2 id="heading-what-makes-trpc-different">What Makes tRPC Different?</h2>
<p>tRPC flips the script entirely. Instead of defining your API in a separate schema language, your TypeScript code is the schema. Here's the same API in tRPC:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Backend (tRPC router)</span>
<span class="hljs-keyword">import</span> { initTRPC } <span class="hljs-keyword">from</span> <span class="hljs-string">'@trpc/server'</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;

<span class="hljs-keyword">const</span> t = initTRPC.create();

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> appRouter = t.router({
  createUser: t.procedure
    .input(z.object({
      name: z.string(),
      email: z.string().email(),
    }))
    .mutation(<span class="hljs-function">(<span class="hljs-params">{ input }</span>) =&gt;</span> {
      <span class="hljs-comment">// Do stuff with user</span>
      <span class="hljs-keyword">return</span> { id: <span class="hljs-number">1</span>, name: input.name, email: input.email };
    }),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> AppRouter = <span class="hljs-keyword">typeof</span> appRouter;
</code></pre>
<p>This is where it gets cool. On your frontend:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Frontend - fully type-safe!</span>
<span class="hljs-keyword">import</span> { createTRPCClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@trpc/client'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { AppRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">'./server'</span>;

<span class="hljs-keyword">const</span> client = createTRPCClient&lt;AppRouter&gt;({
  url: <span class="hljs-string">'http://localhost:3000/trpc'</span>,
});

<span class="hljs-comment">// TypeScript knows EVERYTHING about this call</span>
<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> client.createUser.mutate({
  name: <span class="hljs-string">'Alice'</span>,
  email: <span class="hljs-string">'alice@example.com'</span>
});

<span class="hljs-comment">// user is automatically typed as { id: number; name: string; email: string; }</span>
</code></pre>
<p>No code generation or build step, or GraphQL schema. Just pure TypeScript inference doing its thing. If you rename <code>id</code> to <code>userId</code> in your backend, your frontend will immediately show a TypeScript error. You'll catch it before you even save the file.</p>
<p>This is what we call end-to-end type safety, and it's honestly a great transition.</p>
<h2 id="heading-hono-the-lightweight-challenger">Hono: The Lightweight Challenger</h2>
<p>While tRPC is amazing for full-stack TypeScript apps where you control both ends, <a target="_blank" href="https://hono.dev/">Hono</a> takes a slightly different approach. It's a lightweight web framework that gives you type safety while still being a traditional HTTP framework.</p>
<p>Here's the same example in Hono:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-validator'</span>;

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono();

<span class="hljs-keyword">const</span> userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

app.post(<span class="hljs-string">'/api/users'</span>, zValidator(<span class="hljs-string">'json'</span>, userSchema), <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> { name, email } = c.req.valid(<span class="hljs-string">'json'</span>);
  <span class="hljs-keyword">return</span> c.json({ id: <span class="hljs-number">1</span>, name, email });
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> AppType = <span class="hljs-keyword">typeof</span> app;
</code></pre>
<p>On the frontend, you can use Hono’s RPC client:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { hc } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono/client'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { AppType } <span class="hljs-keyword">from</span> <span class="hljs-string">'./server'</span>;

<span class="hljs-keyword">const</span> client = hc&lt;AppType&gt;(<span class="hljs-string">'http://localhost:3000'</span>);

<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.api.users.$post({
  json: { name: <span class="hljs-string">'Bob'</span>, email: <span class="hljs-string">'bob@example.com'</span> }
});

<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> response.json();
<span class="hljs-comment">// user is fully typed!</span>
</code></pre>
<p>Hono is incredibly fast (it runs on Cloudflare Workers, Deno, Bun, and Node.js), and it gives you that sweet type safety while still being a "regular" HTTP framework. You get RESTful routes, middleware, and all the familiar patterns – just with TypeScript powers.</p>
<h2 id="heading-why-this-matters-these-days">Why This Matters These Days</h2>
<p>You might think to yourself, “Okay, I know what you mean, but why should I care about it?” There’s a reason why these tools are being utilized more now than ever before.</p>
<h3 id="heading-developer-experience-is-essential">Developer experience is essential</h3>
<p>In 2026 and beyond, we’ll no longer accept long feedback loops. The ability to modify your backend code and see what might break on your frontend application without having to run the application will be fantastic for productivity. We’ll spend less time fixing bugs and more time creating new functionalities.</p>
<h3 id="heading-smaller-teams-better-apps">Smaller teams, better apps</h3>
<p>With tRPC or Hono, one developer can create an entire full-stack application with type safety at a very fast pace because they don’t have to switch back and forth between REST documentation and TypeScript interfaces – all the data is flowing to and from their backend code directly to their frontend.</p>
<h3 id="heading-the-end-of-works-on-my-machine">The end of “Works on my machine“</h3>
<p>With type safety, errors are caught at compile time instead of at the time your end-user clicks on a button. This is especially impactful when working in larger teams, when the backend developers and front-end developers may not be in constant communication with one another.</p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Want to try this out? Here's the fastest way:</p>
<p>For tRPC:</p>
<pre><code class="lang-bash">npm create @trpc/next-app@latest
</code></pre>
<p>This scaffolds a Next.js app with tRPC already configured. Check out the <a target="_blank" href="https://trpc.io/docs/client/nextjs">official tRPC docs</a> for more.</p>
<p>For Hono:</p>
<pre><code class="lang-bash">npm create hono@latest
</code></pre>
<p>Pick your runtime (Node.js, Cloudflare Workers, etc.), and you're off to the races. The <a target="_blank" href="https://hono.dev/">Hono documentation</a> is excellent and super approachable.</p>
<h2 id="heading-the-future-is-type-safe">The Future is Type-Safe</h2>
<p>Look, REST isn't going anywhere, and GraphQL has its place. But for full-stack TypeScript developers, tRPC and Hono represent something special: type safety without the ceremony. No code generation or no schema duplication, just TypeScript doing what it does best.</p>
<p>In the future, when you start a new project, give one of these a shot. Your future self – the one who's refactoring code at 2 AM – will thank you.</p>
<p>Happy coding!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Production-Ready Web Apps with the Hono Framework: A Deep Dive ]]>
                </title>
                <description>
                    <![CDATA[ As a dev, you’d probably like to write your application once and not have to worry so much about where it's going to run. This is what the open source framework Hono lets you do, and it’s a game-changer. Hono is a small, incredibly fast web framework... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-production-ready-web-apps-with-hono/</link>
                <guid isPermaLink="false">68bf3ea02c935a9d306bb65a</guid>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ hono ]]>
                    </category>
                
                    <category>
                        <![CDATA[ javascript framework ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Mayur Vekariya ]]>
                </dc:creator>
                <pubDate>Mon, 08 Sep 2025 20:37:52 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757363825321/562644c8-b2b3-4c1c-92c2-736bcade5aac.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>As a dev, you’d probably like to write your application once and not have to worry so much about where it's going to run. This is what the open source framework Hono lets you do, and it’s a game-changer. Hono is a small, incredibly fast web framework that embraces the "write once, run anywhere" philosophy.</p>
<p>The JavaScript ecosystem moves quickly. One minute, we're building monolithic Node.js servers. The next, it's all about serverless functions and running code at the edge on platforms like Cloudflare or Vercel. Staying current can feel like a full-time job.</p>
<p><a target="_blank" href="https://hono.dev/">Hono</a> is built on top of Web Standards – the same <code>Request</code> and <code>Response</code> objects in your browser – which means your code is naturally portable across almost any JavaScript runtime.</p>
<p>This guide is a deep dive into this powerful little framework, designed to help you build real, production-ready applications. We’ll skip the quick "Hello, World!" and jump straight into the patterns and features you will actually use, with plenty of detailed code examples along the way.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-you-will-learn-in-this-guide">What You Will Learn in This Guide</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites-for-following-along">Prerequisites for Following Along</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-a-professional-hono-project">How to Set Up a Professional Hono Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-understand-honos-core-api">How to Understand Hono's Core API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-context-object-in-depth">The Context Object in Depth</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-use-advanced-features-for-production-apps">How to Use Advanced Features for Production Apps</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-deployment-guide-for-hono">Deployment Guide for Hono</a></p>
</li>
</ul>
<h3 id="heading-what-you-will-learn-in-this-guide">What You Will Learn in This Guide</h3>
<p>By the end of this tutorial, you will be able to:</p>
<ul>
<li><p>Structure a Hono project for both development and production.</p>
</li>
<li><p>Implement advanced routing patterns.</p>
</li>
<li><p>Leverage the full power of the <strong>Context</strong> object to manage requests and pass data between middleware.</p>
</li>
<li><p>Write complex custom middleware for authentication, logging, and error handling.</p>
</li>
<li><p>Validate incoming data using the official <strong>Zod</strong> validator for robust APIs.</p>
</li>
<li><p>Build a small, server-rendered application with <strong>JSX</strong> components.</p>
</li>
<li><p>Deploy a Hono application to various modern hosting platforms.</p>
</li>
</ul>
<h3 id="heading-prerequisites-for-following-along">Prerequisites for Following Along</h3>
<p>This is an in-depth guide, but it assumes you have some foundational knowledge. Before you start, you should have:</p>
<ul>
<li><p><strong>Node.js installed:</strong> Version 18 or higher is recommended.</p>
</li>
<li><p><strong>A code editor:</strong> Visual Studio Code is a great choice.</p>
</li>
<li><p><strong>Familiarity with TypeScript:</strong> You should understand basic types, functions, and <code>async</code>/<code>await</code>.</p>
</li>
<li><p><strong>Basic command-line knowledge:</strong> You should be comfortable running commands in your terminal.</p>
</li>
</ul>
<h2 id="heading-how-to-set-up-a-professional-hono-project">How to Set Up a Professional Hono Project</h2>
<p>You can get started with Hono using a single command. This will create a new project directory with a recommended structure and configuration files. When prompted, select the <code>nodejs</code> template and choose to install dependencies with your preferred package manager (for example, npm).</p>
<pre><code class="lang-bash">npm create hono@latest hono-production-app
</code></pre>
<p>The command will guide you through the setup:</p>
<pre><code class="lang-bash">&gt; npx create-hono hono-production-app

create-hono version 0.19.2
✔ Using target directory … hono-production-app
✔ Which template <span class="hljs-keyword">do</span> you want to use? nodejs
✔ Do you want to install project dependencies? Yes
✔ Which package manager <span class="hljs-keyword">do</span> you want to use? npm
✔ Cloning the template
✔ Installing project dependencies
🎉 Copied project files
Get started with: <span class="hljs-built_in">cd</span> hono-production-app
</code></pre>
<p>Now, navigate into your new directory: <code>cd hono-production-app</code>. Let's look at the files that were created:</p>
<ul>
<li><p><code>package.json</code>: Defines your project's dependencies and scripts.</p>
</li>
<li><p><code>tsconfig.json</code>: The TypeScript configuration file.</p>
</li>
<li><p><code>src/index.ts</code>: The entry point of your application.</p>
</li>
</ul>
<p>Now, you can run <code>npm run dev</code> to start your development server. Navigate to <a target="_blank" href="http://localhost:3000"><code>http://localhost:3000</code></a>, and you will see "Hello Hono!".</p>
<h2 id="heading-how-to-understand-honos-core-api">How to Understand Hono's Core API</h2>
<p>Hono's API is designed to be minimal, which makes it easy to learn – yet incredibly powerful.</p>
<h3 id="heading-how-to-use-advanced-routing-techniques">How to Use Advanced Routing Techniques</h3>
<p>You may already know <code>app.get()</code> and <code>app.post()</code> from Express, but Hono's router can do much more.</p>
<h4 id="heading-1-how-to-route-with-regular-expressions">1. How to Route with Regular Expressions</h4>
<p>You can constrain a URL parameter to match a specific regular expression. For example, to make sure an <code>:id</code> parameter only accepts numbers, you can do this:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Only match routes like /users/123, not /users/abc</span>
app.get(<span class="hljs-string">'/users/:id{[0-9]+}'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> id = c.req.param(<span class="hljs-string">'id'</span>)
  <span class="hljs-keyword">return</span> c.text(<span class="hljs-string">`Fetching data for user ID: <span class="hljs-subst">${id}</span>`</span>)
})
</code></pre>
<h4 id="heading-2-how-to-use-optional-and-wildcard-routes">2. How to Use Optional and Wildcard Routes</h4>
<p>You can define routes that match multiple paths using wildcards (<code>*</code>) or handle optional parameters.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// This will match /files/image.png, /files/docs/report.pdf, and so on.</span>
app.get(<span class="hljs-string">'/files/*'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
  <span class="hljs-comment">// c.req.path will contain the full matched path</span>
  <span class="hljs-keyword">return</span> c.text(<span class="hljs-string">`You are accessing the file at: <span class="hljs-subst">${c.req.path}</span>`</span>)
})

<span class="hljs-comment">// The '?' makes the '/:format?' part of the URL optional</span>
<span class="hljs-comment">// This will match both /api/posts and /api/posts/json</span>
app.get(<span class="hljs-string">'/api/posts/:format?'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> format = c.req.param(<span class="hljs-string">'format'</span>)
  <span class="hljs-keyword">if</span> (format === <span class="hljs-string">'json'</span>) {
    <span class="hljs-keyword">return</span> c.json({ message: <span class="hljs-string">'Here are the posts in JSON format.'</span> })
  }
  <span class="hljs-keyword">return</span> c.text(<span class="hljs-string">'Here are the posts in plain text.'</span>)
})
</code></pre>
<h4 id="heading-3-how-to-group-routes-with-approute">3. How to Group Routes with <code>app.route()</code></h4>
<p>For larger applications, you should organize your routes into logical groups. The <code>app.route()</code> method is perfect for this. It allows you to create modular routers and mount them on a specific prefix.</p>
<p>Let's create a more complex API structure for a blog.</p>
<p><code>src/routes/posts.ts</code></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>

<span class="hljs-comment">// Create a new router instance specifically for posts</span>
<span class="hljs-keyword">const</span> posts = <span class="hljs-keyword">new</span> Hono()

posts.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.json({ posts: [] }))
posts.post(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.json({ message: <span class="hljs-string">'Post created'</span> }, <span class="hljs-number">201</span>))
posts.get(<span class="hljs-string">'/:id'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.json({ post: { id: c.req.param(<span class="hljs-string">'id'</span>) } }))

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> posts
</code></pre>
<p><code>src/routes/authors.ts</code></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>

<span class="hljs-keyword">const</span> authors = <span class="hljs-keyword">new</span> Hono()

authors.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.json({ authors: [] }))
authors.get(<span class="hljs-string">'/:id'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.json({ author: { id: c.req.param(<span class="hljs-string">'id'</span>) } }))

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> authors
</code></pre>
<p><code>src/index.ts</code></p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/node-server'</span>
<span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>
<span class="hljs-keyword">import</span> { appendTrailingSlash } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono/trailing-slash'</span>;
<span class="hljs-keyword">import</span> posts <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes/posts.js'</span>
<span class="hljs-keyword">import</span> authors <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes/authors.js'</span>

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono()

app.use(appendTrailingSlash());

app.route(<span class="hljs-string">'/posts/'</span>, posts)
app.route(<span class="hljs-string">'/authors/'</span>, authors)

app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> c.text(<span class="hljs-string">'Hello Hono!'</span>)
})

serve({
  fetch: app.fetch,
  port: <span class="hljs-number">3000</span>
}, <span class="hljs-function">(<span class="hljs-params">info</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server is running on http://localhost:<span class="hljs-subst">${info.port}</span>`</span>)
})
</code></pre>
<p>This pattern keeps your main <code>index.ts</code> file clean and makes your application much easier to navigate and maintain.</p>
<h2 id="heading-the-context-object-in-depth">The Context Object in Depth</h2>
<p>The <strong>Context</strong> (<code>c</code>) is the heart of Hono. It's an object that gets passed to every middleware and route handler, containing all the information related to the current request. It's essentially a container for the request (<code>c.req</code>) methods for creating a response (<code>c.json</code>, <code>c.html</code>, <code>c.text</code>), as well as a special property for passing data between middleware (<code>c.set</code> and <code>c.get</code>).</p>
<p>While this covers its most common and useful properties, the full Context object contains more. For a comprehensive list of all available properties and methods, you can refer to the official <a target="_blank" href="https://hono.dev/docs/api/context">Hono documentation</a>.</p>
<p>Let's explore how you can use the context object to pass data between middleware and handlers, a crucial technique for things like authentication.</p>
<p>The <code>c.set()</code> and <code>c.get()</code> methods allow you to store and retrieve typed data within the context of a single request.</p>
<p>Replace <code>src/index.ts</code> with this example for authentication:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Context, Next } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>

<span class="hljs-comment">// Define a type for the variables we will store in the context</span>
<span class="hljs-keyword">type</span> AppVariables = {
  user: {
    id: <span class="hljs-built_in">string</span>
    name: <span class="hljs-built_in">string</span>
    roles: <span class="hljs-built_in">string</span>[]
  }
}

<span class="hljs-comment">// Use a generic to tell our Hono app about the variables type</span>
<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono&lt;{ Variables: AppVariables }&gt;()

<span class="hljs-comment">// Middleware to "authenticate" a user from a header</span>
<span class="hljs-keyword">const</span> authMiddleware = <span class="hljs-keyword">async</span> (c: Context, next: Next) =&gt; {
  <span class="hljs-keyword">const</span> userId = c.req.header(<span class="hljs-string">'X-User-ID'</span>)
  <span class="hljs-keyword">if</span> (!userId) {
    <span class="hljs-keyword">return</span> c.json({ error: <span class="hljs-string">'Missing X-User-ID header'</span> }, <span class="hljs-number">401</span>)
  }

  <span class="hljs-comment">// In a real app, you would fetch this from a database</span>
  <span class="hljs-keyword">const</span> user = {
    id: userId,
    name: <span class="hljs-string">'Jane Doe'</span>,
    roles: [<span class="hljs-string">'admin'</span>, <span class="hljs-string">'editor'</span>],
  }

  <span class="hljs-comment">// Use c.set() to attach the user data to the context</span>
  c.set(<span class="hljs-string">'user'</span>, user)

  <span class="hljs-keyword">await</span> next()
}

app.get(<span class="hljs-string">'/admin/dashboard'</span>, authMiddleware, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
  <span class="hljs-comment">// Use c.get() to retrieve the typed user data</span>
  <span class="hljs-keyword">const</span> user = c.get(<span class="hljs-string">'user'</span>)

  <span class="hljs-keyword">if</span> (!user.roles.includes(<span class="hljs-string">'admin'</span>)) {
    <span class="hljs-keyword">return</span> c.json({ error: <span class="hljs-string">'Forbidden'</span> }, <span class="hljs-number">403</span>)
  }

  <span class="hljs-keyword">return</span> c.json({
    message: <span class="hljs-string">`Welcome to the admin dashboard, <span class="hljs-subst">${user.name}</span>!`</span>,
    userId: user.id,
  })
})

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> app
</code></pre>
<p>Let's break down the important parts of the code above.</p>
<ul>
<li><p><strong>Typed context variables</strong>: We define a TypeScript type <code>AppVariables</code> and pass it as a generic to our Hono app <code>new Hono&lt;{ Variables: AppVariables }&gt;()</code>. This is a powerful feature that gives us full type-safety for our context variables, preventing typos and ensuring that the data we store and retrieve is exactly what we expect it to be.</p>
</li>
<li><p><strong>Custom middleware</strong>: The <code>authMiddleware</code> is a custom function that runs before our route handler. It inspects the incoming request's headers (<code>c.req.header('X-User-ID')</code>).</p>
</li>
<li><p><strong>Storing data</strong>: If a valid header is found, the middleware uses <code>c.set('user', user)</code> to store the user object on the context. This data is now available to any subsequent middleware or route handler for the same request.</p>
</li>
<li><p><strong>Retrieving data</strong>: The route handler <code>app.get('/admin/dashboard', ...)</code> then uses <code>c.get('user')</code> to retrieve the user object. Hono's type system ensures that <code>c.get('user')</code> returns a variable with the type <code>{ id: string; name: string; roles: string[]; }</code>.</p>
</li>
<li><p><strong>Flow control</strong>: If the user is missing or doesn't have the "admin" role, the middleware or handler can immediately send an error response using <code>c.json()</code> and a status code, preventing the request from proceeding further.</p>
</li>
</ul>
<p>Now, run <code>npm run dev</code>.</p>
<p>You can test with <code>curl</code> (add header):</p>
<pre><code class="lang-bash">curl -H <span class="hljs-string">"X-User-ID: 123"</span> http://localhost:3000/admin/dashboard
</code></pre>
<p>This will return a welcome message.</p>
<p>Without the header:</p>
<pre><code class="lang-bash">curl http://localhost:3000/admin/dashboard
</code></pre>
<p>This will return a <code>401</code> error.</p>
<p>This demonstrates how to pass typed data securely and efficiently between middleware and route handlers.</p>
<h2 id="heading-how-to-use-advanced-features-for-production-apps">How to Use Advanced Features for Production Apps</h2>
<p>Now we're ready to tackle the features you'll use every day in production: advanced middleware, data validation, and building full-stack applications.</p>
<h3 id="heading-how-to-use-advanced-middleware-patterns">How to Use Advanced Middleware Patterns</h3>
<p>Hono has a powerful set of built-in middleware, including JWT and caching. These are not separate libraries you have to install, but rather functions that come with the Hono package itself.</p>
<p><strong>Step 1:</strong> Replace <code>src/index.ts</code> with this example for JWT and caching:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>
<span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/node-server'</span>
<span class="hljs-keyword">import</span> { jwt, sign } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono/jwt'</span>

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono()
<span class="hljs-keyword">const</span> SECRET = <span class="hljs-string">'my-secret-key'</span> <span class="hljs-comment">// Use an environment variable in production!</span>

<span class="hljs-comment">// Create a simple in-memory cache store</span>
<span class="hljs-keyword">const</span> cacheStore = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>();

<span class="hljs-comment">// Custom caching middleware for Node.js</span>
app.use(<span class="hljs-string">'/api/public-data'</span>, <span class="hljs-keyword">async</span> (c, next) =&gt; {
  <span class="hljs-keyword">const</span> cacheKey = c.req.url;

  <span class="hljs-comment">// Check if the response is in our cache</span>
  <span class="hljs-keyword">if</span> (cacheStore.has(cacheKey)) {
    <span class="hljs-keyword">const</span> cachedItem = cacheStore.get(cacheKey);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Serving from custom in-memory cache.'</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(cachedItem.body, { headers: cachedItem.headers });
  }

  <span class="hljs-comment">// If not in cache, proceed to the route handler</span>
  <span class="hljs-keyword">await</span> next();

  <span class="hljs-comment">// After the handler returns, clone and store the response</span>
  <span class="hljs-keyword">if</span> (c.res) {
    <span class="hljs-keyword">const</span> newResponse = c.res.clone();
    <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> newResponse.text();
    <span class="hljs-keyword">const</span> headers = <span class="hljs-built_in">Object</span>.fromEntries(newResponse.headers.entries());
    cacheStore.set(cacheKey, { body, headers });
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Storing response in custom in-memory cache.'</span>);
  }
});

<span class="hljs-comment">// Login to get a JWT</span>
app.post(<span class="hljs-string">'/login'</span>, <span class="hljs-keyword">async</span> (c) =&gt; {
  <span class="hljs-keyword">const</span> { username } = <span class="hljs-keyword">await</span> c.req.json()
  <span class="hljs-keyword">if</span> (username === <span class="hljs-string">'admin'</span>) {
    <span class="hljs-keyword">const</span> payload = {
      sub: username,
      role: <span class="hljs-string">'admin'</span>,
      exp: <span class="hljs-built_in">Math</span>.floor(<span class="hljs-built_in">Date</span>.now() / <span class="hljs-number">1000</span>) + <span class="hljs-number">60</span> * <span class="hljs-number">5</span>, <span class="hljs-comment">// 5 minutes expiration</span>
    }
    <span class="hljs-keyword">const</span> token = <span class="hljs-keyword">await</span> sign(payload, SECRET)
    <span class="hljs-keyword">return</span> c.json({ token })
  }
  <span class="hljs-keyword">return</span> c.json({ error: <span class="hljs-string">'Invalid credentials'</span> }, <span class="hljs-number">401</span>)
})

<span class="hljs-comment">// Protected route</span>
app.get(
  <span class="hljs-string">'/api/protected'</span>,
  jwt({ secret: SECRET }),
  <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> payload = c.get(<span class="hljs-string">'jwtPayload'</span>)
    <span class="hljs-keyword">return</span> c.json({ message: <span class="hljs-string">'You have access!'</span>, payload })
  }
)

<span class="hljs-comment">// Cached route</span>
app.get(
  <span class="hljs-string">'/api/public-data'</span>,
  <span class="hljs-keyword">async</span> (c) =&gt; {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Executing handler with delay...'</span>);
    <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function"><span class="hljs-params">resolve</span> =&gt;</span> <span class="hljs-built_in">setTimeout</span>(resolve, <span class="hljs-number">1000</span>)) <span class="hljs-comment">// Simulate a delay</span>
    <span class="hljs-keyword">return</span> c.json({ data: <span class="hljs-string">'This is some public data that rarely changes.'</span> })
  }
)

serve({ fetch: app.fetch, port: <span class="hljs-number">3000</span> }, <span class="hljs-function">(<span class="hljs-params">info</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server is running on http://localhost:<span class="hljs-subst">${info.port}</span>`</span>)
})
</code></pre>
<p>The code above shows two different types of middleware in action.</p>
<p>First, <strong>JWT middleware</strong> (<code>jwt</code>) is a powerful way to secure your routes. When we call <code>jwt({ secret: SECRET })</code>, we're telling Hono to check for a valid JWT in the <code>Authorization</code> header of the incoming request. If a valid token is found, it decodes the payload and attaches it to the context, where we can retrieve it with <code>c.get('jwtPayload')</code>. If no token is found or if the token is invalid, the middleware automatically stops the request and returns a <code>401 Unauthorized</code> error.</p>
<p>We also have <strong>Custom Cache Middleware</strong> which demonstrates the power of Hono's middleware system for in-memory caching. The middleware first checks an in-memory <code>Map</code> to see if a response for the current URL already exists. If it does, it immediately returns the cached response, preventing the route handler from ever being executed. If the response is not in the cache, it allows the request to continue to the handler. After the handler returns, the middleware intercepts the response and stores a copy in the cache before sending it back to the client. This is a robust and reliable pattern for Node.js environments.</p>
<p><strong>Step 2:</strong> Run <code>npm run dev</code>.</p>
<p><strong>Step 3:</strong> Test the login endpoint with <code>curl</code>:</p>
<p>First, let's test the login endpoint to get a JWT. Open a new terminal and run the following command. The command sends a <code>POST</code> request to the <code>/login</code> endpoint with <code>username: "admin"</code> in the request body.</p>
<pre><code class="lang-bash">curl -X POST http://localhost:3000/login -H <span class="hljs-string">"Content-Type: application/json"</span> -d <span class="hljs-string">'{"username": "admin"}'</span>
</code></pre>
<p>This will return a JSON object with a JWT. Copy this token for the next step.</p>
<p>Now, let's test the protected route. We'll use the token we just received in the <code>Authorization</code> header. Replace <code>&lt;your_jwt_token&gt;</code> with the token you copied.</p>
<pre><code class="lang-bash">curl http://localhost:3000/api/protected -H <span class="hljs-string">"Authorization: Bearer &lt;your_jwt_token&gt;"</span>
</code></pre>
<p>You should get a success message with the decoded payload.</p>
<p>Finally, let's test the cached route. You’ll need to run a production build and run the file with <code>node</code> for this to work.</p>
<p>First, run the following command. The <code>1000</code> millisecond delay in the code will make this request take about a second.</p>
<pre><code class="lang-bash">curl -o /dev/null -s -w <span class="hljs-string">'Total: %{time_total}s\n'</span> http://localhost:3000/api/public-data
</code></pre>
<p>Immediately run the <strong>exact same command again</strong>. This time, the response will be almost instantaneous because our custom cache middleware served the response directly from its in-memory store, completely bypassing the <code>setTimeout</code> in the route handler. Run it a third time, and you'll see a similar near-instantaneous response.</p>
<p>Here's an example of what your terminal output should look like when testing the cache. The first request took around 1 second, but subsequent requests were a matter of milliseconds.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1757125753352/180cc4a7-f361-4966-a26f-a5d8251f77a4.png" alt="180cc4a7-f361-4966-a26f-a5d8251f77a4" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-how-to-create-a-global-error-handler">How to Create a Global Error Handler</h3>
<p>You can define a single global error handler with <code>app.onError()</code>. This is useful for handling unexpected errors in a centralized way, such as validation failures.</p>
<p>Add the following code to your <code>src/index.ts</code>:</p>
<pre><code class="lang-typescript">app.get(<span class="hljs-string">'/users/:id'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> id = c.req.param(<span class="hljs-string">'id'</span>)
  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">isNaN</span>(<span class="hljs-built_in">Number</span>(id))) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'User ID must be a number.'</span>)
  }
  <span class="hljs-keyword">return</span> c.text(<span class="hljs-string">`User ID is <span class="hljs-subst">${id}</span>`</span>)
})

app.onError(<span class="hljs-function">(<span class="hljs-params">err, c</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`<span class="hljs-subst">${err}</span>`</span>)
  <span class="hljs-keyword">return</span> c.json({
    success: <span class="hljs-literal">false</span>,
    message: err.message,
  }, <span class="hljs-number">500</span>)
})
</code></pre>
<p>Now, if you visit <a target="_blank" href="http://localhost:3000/users/abc"><code>http://localhost:3000/users/abc</code></a>, you will get a JSON error response instead of an uncaught exception.</p>
<h3 id="heading-how-to-handle-validation-with-zod">How to Handle Validation with Zod</h3>
<p>For robust APIs, data validation is essential. Hono integrates seamlessly with <a target="_blank" href="https://zod.dev/">Zod</a>, a popular TypeScript-first schema validation library.</p>
<p><strong>Step 1:</strong> Install the necessary dependencies:</p>
<pre><code class="lang-bash">npm install zod @hono/zod-validator
</code></pre>
<p><strong>Step 2:</strong> Replace <code>src/index.ts</code> with the validation example:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>
<span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/node-server'</span>
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-validator'</span>

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono()

<span class="hljs-comment">// Define a Zod schema for the user creation data</span>
<span class="hljs-keyword">const</span> createUserSchema = z.object({
  username: z.string().min(<span class="hljs-number">3</span>).max(<span class="hljs-number">20</span>),
  email: z.string().email(),
  age: z.number().int().positive(),
  tags: z.array(z.string()).optional(),
})

app.post(
  <span class="hljs-string">'/users'</span>,
  zValidator(<span class="hljs-string">'json'</span>, createUserSchema), <span class="hljs-comment">// Use zValidator middleware</span>
  (c) =&gt; {
    <span class="hljs-comment">// The validated data is available on c.req.valid()</span>
    <span class="hljs-keyword">const</span> user = c.req.valid(<span class="hljs-string">'json'</span>)
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Creating user: <span class="hljs-subst">${user.username}</span> with email <span class="hljs-subst">${user.email}</span>`</span>)
    <span class="hljs-keyword">return</span> c.json({
      success: <span class="hljs-literal">true</span>,
      message: <span class="hljs-string">'User created successfully!'</span>,
      user: user,
    }, <span class="hljs-number">201</span>)
  }
)

serve({ fetch: app.fetch, port: <span class="hljs-number">3000</span> }, <span class="hljs-function">(<span class="hljs-params">info</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server is running on http://localhost:<span class="hljs-subst">${info.port}</span>`</span>)
})
</code></pre>
<p>This is how the Zod validation is working:</p>
<ol>
<li><p>We first define a schema called <code>createUserSchema</code> using <code>z.object()</code>. This schema is a blueprint for the expected data structure. We use Zod's built-in methods like <code>z.string().min(3)</code>, <code>z.string().email()</code>, and <code>z.number().int().positive()</code> to specify validation rules for each property. For example, <code>username</code> must be a string between 3 and 20 characters, <code>email</code> must be a valid email format, and <code>age</code> must be a positive integer.</p>
</li>
<li><p>We then apply the <a target="_blank" href="https://github.com/honojs/middleware/tree/main/packages/zod-validator"><code>zValidator</code></a> middleware to our route handler. The first argument, <code>'json'</code>, tells the middleware to validate the incoming request's JSON body. The second argument, <code>createUserSchema</code>, tells it which schema to use for the validation.</p>
</li>
<li><p>The <code>zValidator</code> middleware automatically does the heavy lifting. When a request hits the <code>/users</code> endpoint, it will parse the JSON body and attempt to validate it against <code>createUserSchema</code>. If the data is invalid (for example, the <code>email</code> is not in a valid format), the middleware will immediately stop the request and return a <code>400 Bad Request</code> status with a detailed error message, all without us having to write any manual checks.</p>
</li>
<li><p>If the data is valid, the middleware makes it available on the <code>Context</code> object, which we can access with <code>c.req.valid('json')</code>. Hono's type system ensures that this data is correctly typed according to the Zod schema, so we can use it safely in our handler.</p>
</li>
</ol>
<p><strong>Step 3:</strong> Run <code>npm run dev</code>.</p>
<p><strong>Step 4:</strong> Test with <code>curl</code> (valid data):</p>
<pre><code class="lang-bash">curl -X POST http://localhost:3000/users -H <span class="hljs-string">"Content-Type: application/json"</span> -d <span class="hljs-string">'{"username": "testuser", "email": "test@example.com", "age": 25}'</span>
</code></pre>
<p>This will return a success message.</p>
<p>Test with invalid data (for example, bad email):</p>
<pre><code class="lang-bash">curl -X POST http://localhost:3000/users -H <span class="hljs-string">"Content-Type: application/json"</span> -d <span class="hljs-string">'{"username": "testuser", "email": "invalid-email", "age": 25}'</span>
</code></pre>
<p>This will automatically return a <code>400</code> status with a detailed error message from Zod.</p>
<h3 id="heading-how-to-build-a-full-stack-app-with-jsx">How to Build a Full-Stack App with JSX</h3>
<p>Hono supports server-side rendering with JSX, allowing you to build full-stack applications without needing a separate framework.</p>
<p><strong>Step 1:</strong> Create <code>src/components/Layout.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { html } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono/html'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> Layout = <span class="hljs-function">(<span class="hljs-params">props: { title: <span class="hljs-built_in">string</span>; children?: <span class="hljs-built_in">any</span> }</span>) =&gt;</span> html`<span class="xml">
  <span class="hljs-meta">&lt;!DOCTYPE <span class="hljs-meta-keyword">html</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span></span><span class="hljs-subst">${props.title}</span><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
        <span class="hljs-selector-tag">body</span> { <span class="hljs-attribute">font-family</span>: sans-serif; <span class="hljs-attribute">background</span>: <span class="hljs-number">#f4f4f4</span>; <span class="hljs-attribute">color</span>: <span class="hljs-number">#333</span>; }
        <span class="hljs-selector-class">.container</span> { <span class="hljs-attribute">max-width</span>: <span class="hljs-number">800px</span>; <span class="hljs-attribute">margin</span>: <span class="hljs-number">2rem</span> auto; <span class="hljs-attribute">padding</span>: <span class="hljs-number">1rem</span>; <span class="hljs-attribute">background</span>: white; <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">8px</span>; }
        <span class="hljs-selector-tag">header</span> { <span class="hljs-attribute">border-bottom</span>: <span class="hljs-number">1px</span> solid <span class="hljs-number">#ccc</span>; <span class="hljs-attribute">padding-bottom</span>: <span class="hljs-number">1rem</span>; }
        <span class="hljs-selector-tag">footer</span> { <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">2rem</span>; <span class="hljs-attribute">text-align</span>: center; <span class="hljs-attribute">font-size</span>: <span class="hljs-number">0.8rem</span>; <span class="hljs-attribute">color</span>: <span class="hljs-number">#777</span>; }
      </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"container"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">header</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span></span><span class="hljs-subst">${props.title}</span><span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">header</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">main</span>&gt;</span>
          </span><span class="hljs-subst">${props.children}</span><span class="xml">
        <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">footer</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Powered by Hono<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">footer</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
`</span>
</code></pre>
<p><strong>Step 2:</strong> Create <code>src/components/PostItem.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> PostItem = <span class="hljs-function">(<span class="hljs-params">props: { post: { id: <span class="hljs-built_in">number</span>; title: <span class="hljs-built_in">string</span>; author: <span class="hljs-built_in">string</span> } }</span>) =&gt;</span> (
  &lt;article style=<span class="hljs-string">"border-bottom: 1px solid #eee; padding: 1rem 0;"</span>&gt;
    &lt;h3&gt;&lt;a href={<span class="hljs-string">`/posts/<span class="hljs-subst">${props.post.id}</span>`</span>}&gt;{props.post.title}&lt;<span class="hljs-regexp">/a&gt;&lt;/</span>h3&gt;
    &lt;p&gt;&lt;em&gt;By {props.post.author}&lt;<span class="hljs-regexp">/em&gt;&lt;/</span>p&gt;
  &lt;/article&gt;
)
</code></pre>
<p><strong>Step 3:</strong> Update <code>src/index.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>
<span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/node-server'</span>
<span class="hljs-keyword">import</span> { Layout } <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/Layout'</span>
<span class="hljs-keyword">import</span> { PostItem } <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/PostItem'</span>

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono()

<span class="hljs-comment">// Mock data</span>
<span class="hljs-keyword">const</span> posts = [
  { id: <span class="hljs-number">1</span>, title: <span class="hljs-string">'Getting Started with Hono'</span>, author: <span class="hljs-string">'Alice'</span> },
  { id: <span class="hljs-number">2</span>, title: <span class="hljs-string">'Advanced Middleware Patterns'</span>, author: <span class="hljs-string">'Bob'</span> },
  { id: <span class="hljs-number">3</span>, title: <span class="hljs-string">'Deploying Hono to the Edge'</span>, author: <span class="hljs-string">'Charlie'</span> },
]

app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> c.html(
    &lt;Layout title=<span class="hljs-string">"My Hono Blog"</span>&gt;
      &lt;h2&gt;Recent Posts&lt;/h2&gt;
      {posts.length &gt; <span class="hljs-number">0</span>
        ? posts.map(<span class="hljs-function"><span class="hljs-params">post</span> =&gt;</span> &lt;PostItem post={post} /&gt;)
        : &lt;p&gt;No posts yet!&lt;/p&gt;
      }
    &lt;/Layout&gt;
  )
})

serve({ fetch: app.fetch, port: <span class="hljs-number">3000</span> }, <span class="hljs-function">(<span class="hljs-params">info</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server is running on http://localhost:<span class="hljs-subst">${info.port}</span>`</span>)
})
</code></pre>
<p>Make sure to update the <code>dev</code> script in your <code>package.json</code> file to have <code>src/index.tsx</code> as the starting point.</p>
<pre><code class="lang-json"><span class="hljs-string">"dev"</span>: <span class="hljs-string">"tsx watch src/index.tsx"</span>
</code></pre>
<p><strong>Step 4:</strong> Run <code>npm run dev</code> and visit <a target="_blank" href="http://localhost:3000"><code>http://localhost:3000</code></a>. You will see a fully rendered blog page with the list of posts.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756952439130/27ee63cc-6d60-4372-9634-4d1eadf33f32.png" alt="Blog page with list of posts" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-deployment-guide-for-hono">Deployment Guide for Hono</h2>
<p>You have built your application, and now it's time to share it with the world. Here’s how you can deploy your Hono app to some of the most popular platforms.</p>
<h3 id="heading-how-to-deploy-to-nodejs">How to Deploy to Node.js</h3>
<p>For a traditional server environment, you can use the <code>@hono/node-server</code> adapter and a process manager like <code>pm2</code> for production.</p>
<p><code>src/index.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/node-server'</span>
<span class="hljs-keyword">import</span> app <span class="hljs-keyword">from</span> <span class="hljs-string">'./app'</span> <span class="hljs-comment">// Assuming your Hono app is in app.ts</span>

serve({ fetch: app.fetch, port: <span class="hljs-number">3000</span> })
</code></pre>
<p>You will then build your TypeScript to JavaScript and run <code>pm2 start dist/index.js</code> to run it in the background.</p>
<h3 id="heading-how-to-deploy-to-cloudflare-workers">How to Deploy to Cloudflare Workers</h3>
<p>Hono's true power lies in its portability. The <code>create hono</code> command can set up a project specifically for Cloudflare Workers.</p>
<p>Run the following command and select the <code>cloudflare-workers</code> template:</p>
<pre><code class="lang-bash">npm create hono@latest my-app-hono-cloudflare-worker

create-hono version 0.19.2
✔ Using target directory … my-app-hono-cloudflare-worker
? Which template <span class="hljs-keyword">do</span> you want to use?
  aws-lambda
  bun
❯ cloudflare-workers
  cloudflare-workers+vite
  deno
  fastly
  lambda-edge
</code></pre>
<p>The setup process is identical to the Node.js example, but the project structure is optimized for Cloudflare.</p>
<p>Once the project is set up, you only need to type one command to deploy your application to Cloudflare:</p>
<pre><code class="lang-bash">wrangler deploy
</code></pre>
<p>This command will prompt you to log in to your Cloudflare account and will handle the entire deployment process automatically.</p>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>You've made it! We’ve covered a lot in this guide. You started with a professional project setup and moved all the way through advanced routing, context management, complex middleware patterns, robust data validation, and full-stack JSX components.</p>
<p>You now have the knowledge and the tools to build serious, production-ready applications with Hono. Its simple API doesn't limit its power. Rather, it enhances it by getting out of your way and letting you focus on building great features. And with its helpful portability, you can be confident that the application you build today can be deployed to the platforms of tomorrow.</p>
<p>The web development ecosystem will continue to evolve, but by building on Web Standards, Hono is a framework that's built to last.</p>
<p>To continue your journey, I highly recommend exploring the official <a target="_blank" href="https://hono.dev/docs/">Hono documentation</a>, which is full of even more examples and guides.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
