O que e o Hono
Hono e um framework web leve e rapido desenvolvido no Japao. Funciona em qualquer runtime JavaScript, incluindo Cloudflare Workers, Deno, Bun, Node.js e AWS Lambda.
flowchart TB
subgraph Core["Hono Core (~12KB)"]
Router["Router<br/>(RegExpRouter, TrieRouter)"]
MW["Middleware Chain"]
Ctx["Context<br/>(c.req, c.res)"]
end
Core --> CF["Cloudflare Workers"]
Core --> Deno["Deno Deploy"]
Core --> Bun["Bun"]
CF --> Node["Node.js"]
Deno --> Vercel["Vercel"]
Bun --> Lambda["AWS Lambda"]
Caracteristicas: Compativel com Web Standards API, TypeScript First, Ultra leve
Configuracao
# Criar novo projeto
npm create hono@latest my-app
# Adicionar a projeto existente
npm install hono
# Adaptadores (conforme necessario)
npm install @hono/node-server
Aplicacao Basica
// src/index.ts
import { Hono } from 'hono';
const app = new Hono();
app.get('/', (c) => {
return c.text('Hello Hono!');
});
app.get('/json', (c) => {
return c.json({ message: 'Hello JSON!' });
});
export default app;
Metodos de Inicializacao por Runtime
// Cloudflare Workers
export default app;
// Node.js
import { serve } from '@hono/node-server';
serve(app);
// Bun
export default { port: 3000, fetch: app.fetch };
// Deno
Deno.serve(app.fetch);
Roteamento
import { Hono } from 'hono';
const app = new Hono();
// Rotas basicas
app.get('/users', (c) => c.json({ users: [] }));
app.post('/users', (c) => c.json({ created: true }));
app.put('/users/:id', (c) => c.json({ updated: true }));
app.delete('/users/:id', (c) => c.json({ deleted: true }));
// Parametros de caminho
app.get('/users/:id', (c) => {
const id = c.req.param('id');
return c.json({ id });
});
// Multiplos parametros
app.get('/posts/:postId/comments/:commentId', (c) => {
const { postId, commentId } = c.req.param();
return c.json({ postId, commentId });
});
// Wildcard
app.get('/files/*', (c) => {
const path = c.req.param('*');
return c.text(`File path: ${path}`);
});
// Padrao com expressao regular
app.get('/user/:id{[0-9]+}', (c) => {
const id = c.req.param('id');
return c.json({ id: parseInt(id) });
});
// Parametro opcional
app.get('/posts/:id?', (c) => {
const id = c.req.param('id');
if (id) {
return c.json({ post: { id } });
}
return c.json({ posts: [] });
});
Grupos de Rotas
import { Hono } from 'hono';
const app = new Hono();
// Grupo API
const api = new Hono();
api.get('/users', (c) => c.json({ users: [] }));
api.get('/users/:id', (c) => c.json({ user: {} }));
api.post('/users', (c) => c.json({ created: true }));
// Montar com prefixo
app.route('/api', api);
// Rotas aninhadas
const v1 = new Hono();
const v2 = new Hono();
v1.get('/users', (c) => c.json({ version: 1 }));
v2.get('/users', (c) => c.json({ version: 2 }));
app.route('/api/v1', v1);
app.route('/api/v2', v2);
Middleware
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { compress } from 'hono/compress';
import { etag } from 'hono/etag';
import { secureHeaders } from 'hono/secure-headers';
import { timing } from 'hono/timing';
import { prettyJSON } from 'hono/pretty-json';
const app = new Hono();
// Middlewares integrados
app.use('*', logger());
app.use('*', cors());
app.use('*', compress());
app.use('*', etag());
app.use('*', secureHeaders());
app.use('*', timing());
app.use('*', prettyJSON());
// Middleware customizado
app.use('*', async (c, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
c.res.headers.set('X-Response-Time', `${duration}ms`);
});
// Aplicar apenas em caminhos especificos
app.use('/api/*', async (c, next) => {
console.log('API request:', c.req.url);
await next();
});
Middleware de Autenticacao
import { Hono } from 'hono';
import { jwt } from 'hono/jwt';
import { bearerAuth } from 'hono/bearer-auth';
import { basicAuth } from 'hono/basic-auth';
const app = new Hono();
// Autenticacao JWT
app.use('/api/*', jwt({
secret: process.env.JWT_SECRET!,
}));
app.get('/api/me', (c) => {
const payload = c.get('jwtPayload');
return c.json({ user: payload });
});
// Autenticacao Bearer Token
app.use('/admin/*', bearerAuth({
token: process.env.ADMIN_TOKEN!,
}));
// Autenticacao Basic
app.use('/dashboard/*', basicAuth({
username: 'admin',
password: 'secret',
}));
// Autenticacao customizada
const authMiddleware = async (c: Context, next: Next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return c.json({ error: 'Unauthorized' }, 401);
}
try {
const user = await verifyToken(token);
c.set('user', user);
await next();
} catch {
return c.json({ error: 'Invalid token' }, 401);
}
};
app.use('/api/*', authMiddleware);
Validacao
Integracao com Zod
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const app = new Hono();
// Validacao do corpo da requisicao
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
age: z.number().int().positive().optional(),
});
app.post('/users', zValidator('json', createUserSchema), async (c) => {
const data = c.req.valid('json');
// data e type-safe: { email: string, name: string, age?: number }
return c.json({ user: data });
});
// Validacao de query parameters
const listUsersSchema = z.object({
limit: z.coerce.number().int().min(1).max(100).default(10),
offset: z.coerce.number().int().min(0).default(0),
sort: z.enum(['name', 'created_at']).optional(),
});
app.get('/users', zValidator('query', listUsersSchema), async (c) => {
const { limit, offset, sort } = c.req.valid('query');
return c.json({ limit, offset, sort });
});
// Validacao de parametros de caminho
const userIdSchema = z.object({
id: z.string().uuid(),
});
app.get('/users/:id', zValidator('param', userIdSchema), async (c) => {
const { id } = c.req.valid('param');
return c.json({ id });
});
// Validacao de headers
const headerSchema = z.object({
'x-api-key': z.string().min(32),
});
app.use('/api/*', zValidator('header', headerSchema));
Erros de Validacao Customizados
import { zValidator } from '@hono/zod-validator';
app.post(
'/users',
zValidator('json', createUserSchema, (result, c) => {
if (!result.success) {
return c.json(
{
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid request body',
details: result.error.issues.map((issue) => ({
field: issue.path.join('.'),
message: issue.message,
})),
},
},
400
);
}
}),
async (c) => {
const data = c.req.valid('json');
return c.json({ user: data });
}
);
Hono RPC
Permite comunicacao type-safe entre cliente e servidor.
// server.ts
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const app = new Hono()
.get('/users', async (c) => {
const users = await db.users.findMany();
return c.json({ users });
})
.get('/users/:id', async (c) => {
const id = c.req.param('id');
const user = await db.users.findById(id);
return c.json({ user });
})
.post(
'/users',
zValidator('json', z.object({
email: z.string().email(),
name: z.string(),
})),
async (c) => {
const data = c.req.valid('json');
const user = await db.users.create(data);
return c.json({ user }, 201);
}
);
export type AppType = typeof app;
export default app;
// client.ts
import { hc } from 'hono/client';
import type { AppType } from './server';
const client = hc<AppType>('http://localhost:3000');
// Chamadas de API type-safe
async function main() {
// GET /users
const res1 = await client.users.$get();
const data1 = await res1.json(); // Tipos inferidos
// GET /users/:id
const res2 = await client.users[':id'].$get({
param: { id: '123' },
});
const data2 = await res2.json();
// POST /users
const res3 = await client.users.$post({
json: {
email: 'user@example.com',
name: 'John Doe',
},
});
const data3 = await res3.json();
}
Upload de Arquivos
import { Hono } from 'hono';
const app = new Hono();
app.post('/upload', async (c) => {
const body = await c.req.parseBody();
const file = body['file'];
if (file instanceof File) {
const buffer = await file.arrayBuffer();
const filename = file.name;
const contentType = file.type;
const size = file.size;
// Upload para S3 ou R2
await uploadToStorage(buffer, filename);
return c.json({
filename,
contentType,
size,
url: `https://cdn.example.com/${filename}`,
});
}
return c.json({ error: 'No file uploaded' }, 400);
});
// Upload de multiplos arquivos
app.post('/upload-multiple', async (c) => {
const body = await c.req.parseBody({ all: true });
const files = body['files'];
if (Array.isArray(files)) {
const results = await Promise.all(
files.map(async (file) => {
if (file instanceof File) {
const buffer = await file.arrayBuffer();
await uploadToStorage(buffer, file.name);
return { filename: file.name, size: file.size };
}
return null;
})
);
return c.json({ uploaded: results.filter(Boolean) });
}
return c.json({ error: 'No files uploaded' }, 400);
});
Tratamento de Erros
import { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
const app = new Hono();
// Classes de erro customizadas
class NotFoundError extends HTTPException {
constructor(resource: string) {
super(404, { message: `${resource} not found` });
}
}
class ValidationError extends HTTPException {
constructor(details: unknown[]) {
super(400, {
message: 'Validation failed',
cause: { details },
});
}
}
// Handler de erros
app.onError((err, c) => {
console.error('Error:', err);
if (err instanceof HTTPException) {
return c.json(
{
error: {
code: err.status === 404 ? 'NOT_FOUND' : 'ERROR',
message: err.message,
details: err.cause,
},
},
err.status
);
}
return c.json(
{
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred',
},
},
500
);
});
// Handler 404
app.notFound((c) => {
return c.json(
{
error: {
code: 'NOT_FOUND',
message: 'Route not found',
},
},
404
);
});
// Exemplo de uso
app.get('/users/:id', async (c) => {
const user = await db.users.findById(c.req.param('id'));
if (!user) {
throw new NotFoundError('User');
}
return c.json({ user });
});
Integracao com Cloudflare Workers
// src/index.ts
import { Hono } from 'hono';
type Bindings = {
DB: D1Database;
KV: KVNamespace;
R2: R2Bucket;
AI: Ai;
};
const app = new Hono<{ Bindings: Bindings }>();
// Banco de dados D1
app.get('/users', async (c) => {
const { results } = await c.env.DB.prepare(
'SELECT * FROM users LIMIT 10'
).all();
return c.json({ users: results });
});
// Armazenamento KV
app.get('/cache/:key', async (c) => {
const key = c.req.param('key');
const value = await c.env.KV.get(key);
return c.json({ key, value });
});
app.put('/cache/:key', async (c) => {
const key = c.req.param('key');
const { value, ttl } = await c.req.json();
await c.env.KV.put(key, value, { expirationTtl: ttl });
return c.json({ success: true });
});
// Armazenamento de objetos R2
app.get('/files/:key', async (c) => {
const key = c.req.param('key');
const object = await c.env.R2.get(key);
if (!object) {
return c.json({ error: 'Not found' }, 404);
}
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
return new Response(object.body, { headers });
});
// Workers AI
app.post('/ai/summarize', async (c) => {
const { text } = await c.req.json();
const response = await c.env.AI.run('@cf/meta/llama-2-7b-chat-int8', {
messages: [
{ role: 'system', content: 'Summarize the following text.' },
{ role: 'user', content: text },
],
});
return c.json({ summary: response.response });
});
export default app;
Testes
// src/index.test.ts
import { describe, it, expect } from 'vitest';
import app from './index';
describe('API Tests', () => {
it('GET / returns Hello', async () => {
const res = await app.request('/');
expect(res.status).toBe(200);
expect(await res.text()).toBe('Hello Hono!');
});
it('GET /users returns user list', async () => {
const res = await app.request('/users');
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toHaveProperty('users');
});
it('POST /users creates a user', async () => {
const res = await app.request('/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'test@example.com',
name: 'Test User',
}),
});
expect(res.status).toBe(201);
const data = await res.json();
expect(data.user.email).toBe('test@example.com');
});
it('POST /users returns 400 for invalid data', async () => {
const res = await app.request('/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'invalid-email',
}),
});
expect(res.status).toBe(400);
});
});