dev_tools
Back to guides

Hono.js + Node.js REST API

Build an ultra-fast, type-safe REST API with Hono.js and TypeScript on Node.js — with Zod validation, structured error handling, and organized route files.

Hono
Node.js
TypeScript
REST API
Zod
1

Initialize the Project

Create a new Node.js project with TypeScript configured for ESM — Hono works best with the native ES Module format.

mkdir my-hono-api && cd my-hono-api && npm init -y && npm install -D typescript tsx @types/node
2

Install Hono and Dependencies

Install Hono core, the @hono/node-server adapter (required for Node.js), Zod for validation, and @hono/zod-validator for the typed middleware.

npm install hono @hono/node-server zod @hono/zod-validator
3

Configure tsconfig.json

Configure TypeScript to compile to ESNext modules, which is required for Hono on Node.js.

tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  },
  "include": ["src"]
}
4

Add npm Scripts

Add dev (runs with tsx for hot reload), build, and start scripts to package.json.

package.json (scripts)
"type": "module",
"scripts": {
  "dev":   "tsx watch src/index.ts",
  "build": "tsc",
  "start": "node dist/index.js"
}
5

Define Zod Schemas

Create src/schemas/user.ts. Centralizing schemas means the same validation rules are used in route handlers and can be re-used for response types.

src/schemas/user.ts
import { z } from 'zod'

export const createUserSchema = z.object({
  name:  z.string().min(1, 'Name is required'),
  email: z.string().email('Invalid email address'),
  role:  z.enum(['user', 'admin']).default('user'),
})

export const updateUserSchema = createUserSchema.partial()

export type CreateUser = z.infer<typeof createUserSchema>
export type UpdateUser = z.infer<typeof updateUserSchema>
6

Create the Users Router

Create src/routes/users.ts. Use @hono/zod-validator to validate request bodies — invalid payloads are rejected before your handler runs, and the parsed data is fully typed.

src/routes/users.ts
import { Hono }         from 'hono'
import { zValidator }  from '@hono/zod-validator'
import { createUserSchema, updateUserSchema } from '../schemas/user.js'

"text-muted-foreground italic">// In-memory store — swap for a real DB in production
const db = new Map<number, { id: number; name: string; email: string; role: string }>()
let nextId = 1

const users = new Hono()

"text-muted-foreground italic">// GET /users
users.get('/', (c) => {
  return c.json(Array.from(db.values()))
})

"text-muted-foreground italic">// GET /users/:id
users.get('/:id', (c) => {
  const id   = Number(c.req.param('id'))
  const user = db.get(id)
  if (!user) return c.json({ error: 'User not found' }, 404)
  return c.json(user)
})

"text-muted-foreground italic">// POST /users
users.post('/', zValidator('json', createUserSchema), (c) => {
  const body = c.req.valid('json')  "text-muted-foreground italic">// fully typed CreateUser
  const user = { id: nextId++, ...body }
  db.set(user.id, user)
  return c.json(user, 201)
})

"text-muted-foreground italic">// PATCH /users/:id
users.patch('/:id', zValidator('json', updateUserSchema), (c) => {
  const id   = Number(c.req.param('id'))
  const user = db.get(id)
  if (!user) return c.json({ error: 'User not found' }, 404)
  const updated = { ...user, ...c.req.valid('json') }
  db.set(id, updated)
  return c.json(updated)
})

"text-muted-foreground italic">// DELETE /users/:id
users.delete('/:id', (c) => {
  const id = Number(c.req.param('id'))
  if (!db.delete(id)) return c.json({ error: 'User not found' }, 404)
  return c.body(null, 204)
})

export { users }
7

Create the Main App

Create src/index.ts. Wire up global middleware (logger, CORS, pretty JSON in dev), mount the users router, and add a catch-all error handler.

src/index.ts
import { serve }  from '@hono/node-server'
import { Hono }   from 'hono'
import { logger } from 'hono/logger'
import { cors }   from 'hono/cors'
import { prettyJSON } from 'hono/pretty-json'
import { HTTPException } from 'hono/http-exception'
import { users } from './routes/users.js'

const app = new Hono()

"text-muted-foreground italic">// Global middleware
app.use('*', logger())
app.use('*', cors())

if (process.env.NODE_ENV !== 'production') {
  app.use('*', prettyJSON())
}

"text-muted-foreground italic">// Routes
app.get('/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() }))
app.route('/users', users)

"text-muted-foreground italic">// Global error handler
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return c.json({ error: err.message }, err.status)
  }
  console.error(err)
  return c.json({ error: 'Internal Server Error' }, 500)
})

"text-muted-foreground italic">// 404 handler
app.notFound((c) => c.json({ error: `Route ${c.req.path} not found` }, 404))

const PORT = Number(process.env.PORT) || 3000

serve({ fetch: app.fetch, port: PORT }, () => {
  console.log(`Server running at http://localhost:${PORT}`)
})
8

Start the Development Server

Run the server. tsx watch restarts automatically on file changes — no extra tooling needed.

npm run dev
💡Try `curl -X POST http://localhost:3000/users -H 'Content-Type: application/json' -d '{"name":"Alice","email":"alice@example.com"}'` to create your first user.
9

Test the API Endpoints

Make a few requests to confirm everything is working. The Zod validator will return a 400 with detailed errors if the body is malformed.

test.sh
# Create a user
curl -s -X POST http://localhost:3000/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"Alice","email":"alice@example.com"}' | jq

# List all users
curl -s http://localhost:3000/users | jq

# Update a user
curl -s -X PATCH http://localhost:3000/users/1 \
  -H 'Content-Type: application/json' \
  -d '{"role":"admin"}' | jq

# Trigger a validation error (missing email)
curl -s -X POST http://localhost:3000/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"Bob"}' | jq

Found this guide helpful? Check out more tools and guides.

Browse All Guides