Tutorial Hono.js dan Cloudflare Workers: Buat REST API Ultrafast
Kamis, 25 Des 2025
Kamu mau bikin REST API yang bisa handle ribuan request dengan latency di bawah 50ms? Dan deploynya gratis? Hono.js + Cloudflare Workers jawabannya.
Hono.js (炎 yang artinya “api” dalam bahasa Jepang) adalah web framework yang didesain khusus untuk edge runtime. Ultrafast, lightweight, dan developer experience-nya juara.
Versi yang Digunakan
| Package | Versi |
|---|---|
| Node.js | 18+ |
| Wrangler | 3.x+ |
| Hono | 4.x |
| Vitest | 3.2.x |
Kenapa Hono.js?
Sebelum kita mulai coding, mari kita pahami kenapa Hono.js layak jadi pilihan:
- Ultrafast - Benchmark menunjukkan Hono 2-3x lebih cepat dari Express
- Edge-first - Didesain untuk edge runtime (Cloudflare Workers, Deno, Bun)
- Tiny - Cuma ~14KB, tidak ada dependency
- TypeScript-first - Type inference yang excellent
- Multi-runtime - Bisa jalan di Node.js, Deno, Bun, Cloudflare Workers
// Hello World dengan Hono - simple banget
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono!'))
export default app
Apa itu Cloudflare Workers?
Cloudflare Workers adalah serverless platform yang menjalankan code di edge network Cloudflare. Artinya, API kamu jalan di 300+ data center di seluruh dunia, dekat dengan user.
Keuntungan Cloudflare Workers:
| Fitur | Keterangan |
|---|---|
| 0ms cold start | Tidak ada cold start seperti AWS Lambda |
| Global edge | Deploy otomatis ke 300+ lokasi |
| Free tier murah hati | 100,000 request/hari gratis |
| D1 Database | SQLite database di edge |
| KV Storage | Key-value store dengan latency rendah |
| Durable Objects | Stateful serverless dengan consistency |
Hono vs Express: Perbandingan
Kalau kamu familiar dengan Express, berikut perbandingannya:
// Express.js
import express from 'express'
const app = express()
app.get('/users/:id', (req, res) => {
const { id } = req.params
res.json({ id, name: 'John' })
})
app.listen(3000)
// Hono.js
import { Hono } from 'hono'
const app = new Hono()
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id, name: 'John' })
})
export default app
| Aspek | Express | Hono |
|---|---|---|
| Bundle Size | ~2MB dengan deps | ~14KB |
| Cold Start | 200-500ms | 0ms di Workers |
| Runtime | Node.js only | Multi-runtime |
| TypeScript | Butuh setup | Built-in |
| Middleware | Callback-based | Modern async |
Setup Project dengan Wrangler CLI
Mari kita mulai bikin project REST API untuk todo app.
1. Install Wrangler
Wrangler adalah CLI untuk develop dan deploy Cloudflare Workers.
npm install -g wrangler
# atau
pnpm add -g wrangler
2. Login ke Cloudflare
wrangler login
Ini akan buka browser untuk autentikasi dengan akun Cloudflare kamu.
3. Buat Project Baru
npm create hono@latest my-api
Tip: Untuk CI/CD atau non-interactive environment, gunakan flag
--template:npm create hono@latest my-api -- --template cloudflare-workers
Pilih cloudflare-workers sebagai template:
? Which template do you want to use?
aws-lambda
bun
❯ cloudflare-workers
cloudflare-pages
deno
fastly
nodejs
4. Struktur Project
Setelah create, struktur project seperti ini:
my-api/
├── src/
│ └── index.ts # Entry point
├── wrangler.toml # Cloudflare config
├── package.json
└── tsconfig.json
5. Install Dependencies
cd my-api
npm install
6. Jalankan Development Server
npm run dev
Buka http://localhost:8787 dan kamu akan lihat “Hello Hono!”.
Basic Routing dengan Hono
Hono punya routing yang intuitif. Mari kita explore:
Route Methods
// filepath: /src/index.ts
import { Hono } from 'hono'
const app = new Hono()
// GET request
app.get('/users', (c) => c.json({ users: [] }))
// POST request
app.post('/users', async (c) => {
const body = await c.req.json()
return c.json({ created: body }, 201)
})
// PUT request
app.put('/users/:id', async (c) => {
const id = c.req.param('id')
const body = await c.req.json()
return c.json({ updated: { id, ...body } })
})
// DELETE request
app.delete('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ deleted: id })
})
export default app
Route Parameters
// Single parameter
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id })
})
// Multiple parameters
app.get('/users/:userId/posts/:postId', (c) => {
const { userId, postId } = c.req.param()
return c.json({ userId, postId })
})
// Optional parameter
app.get('/posts/:id?', (c) => {
const id = c.req.param('id')
if (id) {
return c.json({ post: id })
}
return c.json({ posts: [] })
})
Query Parameters
// GET /search?q=hono&limit=10
app.get('/search', (c) => {
const query = c.req.query('q')
const limit = c.req.query('limit')
return c.json({
query,
limit: parseInt(limit || '10')
})
})
// Get all query params
app.get('/filter', (c) => {
const queries = c.req.queries()
return c.json(queries)
})
Route Groups
import { Hono } from 'hono'
const app = new Hono()
// API v1 routes
const v1 = new Hono()
v1.get('/users', (c) => c.json({ version: 'v1', users: [] }))
v1.get('/posts', (c) => c.json({ version: 'v1', posts: [] }))
// API v2 routes
const v2 = new Hono()
v2.get('/users', (c) => c.json({ version: 'v2', users: [] }))
// Mount ke main app
app.route('/api/v1', v1)
app.route('/api/v2', v2)
export default app
Middleware: CORS, Auth, Logger
Middleware di Hono sangat powerful dan mudah digunakan.
Built-in Middleware
Hono punya banyak middleware built-in:
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { prettyJSON } from 'hono/pretty-json'
import { secureHeaders } from 'hono/secure-headers'
const app = new Hono()
// Logger - log semua request
app.use('*', logger())
// Pretty JSON - format response JSON
app.use('*', prettyJSON())
// Secure Headers - tambah security headers
app.use('*', secureHeaders())
// CORS - allow cross-origin requests
app.use('/api/*', cors({
origin: ['https://myapp.com', 'http://localhost:3000'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400,
}))
Custom Middleware
import { Hono, Next } from 'hono'
import { Context } from 'hono'
const app = new Hono()
// Timing middleware
const timing = async (c: Context, next: Next) => {
const start = Date.now()
await next()
const duration = Date.now() - start
c.header('X-Response-Time', `${duration}ms`)
}
app.use('*', timing)
// Request ID middleware
const requestId = async (c: Context, next: Next) => {
const id = crypto.randomUUID()
c.set('requestId', id)
c.header('X-Request-ID', id)
await next()
}
app.use('*', requestId)
Authentication Middleware
import { Hono } from 'hono'
import { bearerAuth } from 'hono/bearer-auth'
import { jwt } from 'hono/jwt'
const app = new Hono()
// Simple Bearer Token Auth
app.use('/api/*', bearerAuth({ token: 'my-secret-token' }))
// JWT Auth (lebih secure)
app.use('/api/*', jwt({
secret: 'my-jwt-secret',
}))
// Custom auth middleware
const authMiddleware = async (c: Context, next: Next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return c.json({ error: 'Unauthorized' }, 401)
}
const token = authHeader.substring(7)
try {
// Verify token (contoh sederhana)
const payload = await verifyToken(token)
c.set('user', payload)
await next()
} catch {
return c.json({ error: 'Invalid token' }, 401)
}
}
app.use('/protected/*', authMiddleware)
CRUD API Example: Todo App
Mari buat REST API lengkap untuk todo app:
// filepath: /src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { validator } from 'hono/validator'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
// Types
interface Todo {
id: string
title: string
completed: boolean
createdAt: string
}
type Bindings = {
DB: D1Database
}
const app = new Hono<{ Bindings: Bindings }>()
// Middleware
app.use('*', logger())
app.use('*', cors())
// Validation schema
const createTodoSchema = z.object({
title: z.string().min(1).max(200),
})
const updateTodoSchema = z.object({
title: z.string().min(1).max(200).optional(),
completed: z.boolean().optional(),
})
// Routes
// GET /todos - List all todos
app.get('/todos', async (c) => {
const { results } = await c.env.DB.prepare(
'SELECT * FROM todos ORDER BY created_at DESC'
).all<Todo>()
return c.json({ todos: results })
})
// GET /todos/:id - Get single todo
app.get('/todos/:id', async (c) => {
const id = c.req.param('id')
const todo = await c.env.DB.prepare(
'SELECT * FROM todos WHERE id = ?'
).bind(id).first<Todo>()
if (!todo) {
return c.json({ error: 'Todo not found' }, 404)
}
return c.json({ todo })
})
// POST /todos - Create todo
app.post('/todos', zValidator('json', createTodoSchema), async (c) => {
const { title } = c.req.valid('json')
const id = crypto.randomUUID()
const createdAt = new Date().toISOString()
await c.env.DB.prepare(
'INSERT INTO todos (id, title, completed, created_at) VALUES (?, ?, ?, ?)'
).bind(id, title, false, createdAt).run()
return c.json({
todo: { id, title, completed: false, createdAt }
}, 201)
})
// PUT /todos/:id - Update todo
app.put('/todos/:id', zValidator('json', updateTodoSchema), async (c) => {
const id = c.req.param('id')
const updates = c.req.valid('json')
// Check if exists
const existing = await c.env.DB.prepare(
'SELECT * FROM todos WHERE id = ?'
).bind(id).first<Todo>()
if (!existing) {
return c.json({ error: 'Todo not found' }, 404)
}
// Build update query
const newTitle = updates.title ?? existing.title
const newCompleted = updates.completed ?? existing.completed
await c.env.DB.prepare(
'UPDATE todos SET title = ?, completed = ? WHERE id = ?'
).bind(newTitle, newCompleted, id).run()
return c.json({
todo: { ...existing, title: newTitle, completed: newCompleted }
})
})
// DELETE /todos/:id - Delete todo
app.delete('/todos/:id', async (c) => {
const id = c.req.param('id')
const result = await c.env.DB.prepare(
'DELETE FROM todos WHERE id = ?'
).bind(id).run()
if (result.meta.changes === 0) {
return c.json({ error: 'Todo not found' }, 404)
}
return c.json({ deleted: id })
})
// Error handling
app.onError((err, c) => {
console.error(`Error: ${err.message}`)
return c.json({ error: 'Internal Server Error' }, 500)
})
// 404 handler
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404)
})
export default app
Install Zod untuk validation:
npm install zod @hono/zod-validator
Note: Jika terjadi dependency conflict, gunakan flag
--legacy-peer-depsatau pin versi zod:npm install [email protected] @hono/zod-validator
Connect ke D1 Database
D1 adalah SQLite database dari Cloudflare yang berjalan di edge. Perfect untuk aplikasi yang butuh database dengan latency rendah.
1. Buat D1 Database
wrangler d1 create todo-db
Output akan memberikan database ID:
✅ Successfully created DB 'todo-db'
[[d1_databases]]
binding = "DB"
database_name = "todo-db"
database_id = "xxxx-xxxx-xxxx-xxxx"
2. Update wrangler.toml
# filepath: /wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "DB"
database_name = "todo-db"
database_id = "xxxx-xxxx-xxxx-xxxx"
3. Buat Schema
Buat file schema.sql:
-- filepath: /schema.sql
DROP TABLE IF EXISTS todos;
CREATE TABLE todos (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
completed INTEGER DEFAULT 0,
created_at TEXT NOT NULL
);
-- Sample data
INSERT INTO todos (id, title, completed, created_at) VALUES
('1', 'Belajar Hono.js', 0, '2024-01-01T00:00:00Z'),
('2', 'Deploy ke Cloudflare', 0, '2024-01-01T00:00:00Z');
4. Apply Schema
Untuk local development:
wrangler d1 execute todo-db --local --file=./schema.sql
Untuk production:
wrangler d1 execute todo-db --file=./schema.sql
5. Query Database
// Type bindings
type Bindings = {
DB: D1Database
}
const app = new Hono<{ Bindings: Bindings }>()
// SELECT
app.get('/todos', async (c) => {
const { results } = await c.env.DB.prepare(
'SELECT * FROM todos WHERE completed = ?'
).bind(0).all()
return c.json(results)
})
// INSERT
app.post('/todos', async (c) => {
const { title } = await c.req.json()
const result = await c.env.DB.prepare(
'INSERT INTO todos (id, title, created_at) VALUES (?, ?, ?)'
).bind(crypto.randomUUID(), title, new Date().toISOString()).run()
return c.json({ success: result.success })
})
// Batch queries
app.post('/batch', async (c) => {
const results = await c.env.DB.batch([
c.env.DB.prepare('SELECT * FROM todos'),
c.env.DB.prepare('SELECT COUNT(*) as count FROM todos'),
])
return c.json(results)
})
Environment Variables dan Secrets
1. Vars di wrangler.toml
Untuk non-sensitive config:
# filepath: /wrangler.toml
name = "my-api"
main = "src/index.ts"
[vars]
ENVIRONMENT = "development"
API_VERSION = "v1"
2. Secrets dengan Wrangler
Untuk sensitive data seperti API keys:
# Set secret
wrangler secret put JWT_SECRET
# Akan prompt untuk input value
Enter a secret value: ********
3. Akses di Code
type Bindings = {
DB: D1Database
JWT_SECRET: string
ENVIRONMENT: string
API_VERSION: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/config', (c) => {
// Akses vars
const env = c.env.ENVIRONMENT
const version = c.env.API_VERSION
return c.json({ env, version })
})
app.use('/api/*', async (c, next) => {
// Akses secret
const secret = c.env.JWT_SECRET
// Use for JWT verification...
await next()
})
4. Environment-specific Config
# filepath: /wrangler.toml
name = "my-api"
main = "src/index.ts"
# Development
[env.dev.vars]
ENVIRONMENT = "development"
LOG_LEVEL = "debug"
# Production
[env.production.vars]
ENVIRONMENT = "production"
LOG_LEVEL = "error"
Deploy ke environment specific:
# Development
wrangler deploy --env dev
# Production
wrangler deploy --env production
Deploy ke Cloudflare Workers
1. Build dan Deploy
npm run deploy
# atau
wrangler deploy
Output:
⛅️ wrangler 3.x.x
-------------------
Uploaded my-api (1.23 sec)
Published my-api (0.12 sec)
https://my-api.your-subdomain.workers.dev
2. Custom Domain
Di Cloudflare Dashboard:
- Buka Workers & Pages
- Pilih worker kamu
- Tab “Triggers”
- Add Custom Domain
Atau via wrangler.toml:
routes = [
{ pattern = "api.yourdomain.com/*", zone_name = "yourdomain.com" }
]
3. Preview Deployments
# Deploy ke preview URL (tidak affect production)
wrangler deploy --env preview
4. Rollback
# List deployments
wrangler deployments list
# Rollback ke version sebelumnya
wrangler rollback
Testing dengan Vitest
Hono support testing dengan Vitest out of the box.
1. Setup Vitest
npm install -D vitest@~3.2.0 @cloudflare/vitest-pool-workers
Note: Package
@cloudflare/vitest-pool-workerssaat ini hanya support Vitest versi 2.0.x - 3.2.x. Pastikan untuk pin versi Vitest seperti contoh di atas.
2. Konfigurasi Vitest
// filepath: /vitest.config.ts
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'
export default defineWorkersConfig({
test: {
poolOptions: {
workers: {
wrangler: { configPath: './wrangler.toml' },
},
},
},
})
3. Tulis Tests
// filepath: /src/index.test.ts
import { describe, it, expect, beforeAll } from 'vitest'
import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test'
import app from './index'
describe('Todo API', () => {
describe('GET /todos', () => {
it('should return empty array initially', async () => {
const res = await app.request('/todos', {}, env)
expect(res.status).toBe(200)
const data = await res.json()
expect(data.todos).toBeInstanceOf(Array)
})
})
describe('POST /todos', () => {
it('should create a new todo', async () => {
const res = await app.request('/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Test todo' }),
}, env)
expect(res.status).toBe(201)
const data = await res.json()
expect(data.todo.title).toBe('Test todo')
expect(data.todo.completed).toBe(false)
})
it('should return 400 for invalid input', async () => {
const res = await app.request('/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: '' }),
}, env)
expect(res.status).toBe(400)
})
})
describe('PUT /todos/:id', () => {
it('should update todo', async () => {
// Create todo first
const createRes = await app.request('/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Original' }),
}, env)
const { todo } = await createRes.json()
// Update it
const updateRes = await app.request(`/todos/${todo.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Updated', completed: true }),
}, env)
expect(updateRes.status).toBe(200)
const data = await updateRes.json()
expect(data.todo.title).toBe('Updated')
expect(data.todo.completed).toBe(true)
})
})
describe('DELETE /todos/:id', () => {
it('should delete todo', async () => {
// Create todo first
const createRes = await app.request('/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'To Delete' }),
}, env)
const { todo } = await createRes.json()
// Delete it
const deleteRes = await app.request(`/todos/${todo.id}`, {
method: 'DELETE',
}, env)
expect(deleteRes.status).toBe(200)
// Verify deleted
const getRes = await app.request(`/todos/${todo.id}`, {}, env)
expect(getRes.status).toBe(404)
})
})
})
4. Run Tests
npm test
# atau
vitest run
Tambahkan script di package.json:
{
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"test": "vitest run",
"test:watch": "vitest"
}
}
Bonus: OpenAPI Documentation
Hono punya package untuk generate OpenAPI spec:
npm install @hono/zod-openapi
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi'
const app = new OpenAPIHono()
const getTodosRoute = createRoute({
method: 'get',
path: '/todos',
responses: {
200: {
content: {
'application/json': {
schema: z.object({
todos: z.array(z.object({
id: z.string(),
title: z.string(),
completed: z.boolean(),
})),
}),
},
},
description: 'List of todos',
},
},
})
app.openapi(getTodosRoute, async (c) => {
// Your handler
})
// Generate OpenAPI JSON
app.doc('/doc', {
openapi: '3.0.0',
info: {
title: 'Todo API',
version: '1.0.0',
},
})
// Swagger UI
app.get('/ui', swaggerUI({ url: '/doc' }))
Kesimpulan
Hono.js + Cloudflare Workers adalah combo yang powerful untuk bikin REST API:
- Setup mudah - Cuma butuh beberapa menit
- Developer experience - TypeScript first, middleware yang intuitive
- Performance - Ultrafast dengan 0ms cold start
- Scalable - Auto-scale di 300+ edge locations
- Cost effective - 100K request/hari gratis
Untuk project selanjutnya, kamu bisa explore:
- Hono RPC - Type-safe API calls antara client dan server
- KV Storage - Untuk caching dan session
- Queues - Background job processing
- Durable Objects - Stateful serverless
Happy coding! 🔥