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';
import { jwt, sign } from 'hono/jwt';
import { validator } from 'hono/validator';
import { HTTPException } from 'hono/http-exception';
// ============================================
// Types
// ============================================
interface User {
id: number;
email: string;
name: string;
createdAt: Date;
}
interface Post {
id: number;
title: string;
content: string;
authorId: number;
published: boolean;
createdAt: Date;
}
interface Env {
JWT_SECRET: string;
DATABASE_URL?: string;
}
interface Variables {
user?: { id: number; email: string };
}
// ============================================
// In-Memory Database
// ============================================
class Database {
private users = new Map<number, User & { passwordHash: string }>();
private posts = new Map<number, Post>();
private nextUserId = 1;
private nextPostId = 1;
createUser(email: string, name: string, password: string): User {
const user = {
id: this.nextUserId++,
email,
name,
passwordHash: `hashed-${password}`, // Use bcrypt in production
createdAt: new Date(),
};
this.users.set(user.id, user);
const { passwordHash, ...publicUser } = user;
return publicUser;
}
getUser(id: number): User | undefined {
const user = this.users.get(id);
if (!user) return undefined;
const { passwordHash, ...publicUser } = user;
return publicUser;
}
getUserByEmail(email: string): (User & { passwordHash: string }) | undefined {
return Array.from(this.users.values()).find(u => u.email === email);
}
getAllUsers(): User[] {
return Array.from(this.users.values()).map(({ passwordHash, ...u }) => u);
}
createPost(title: string, content: string, authorId: number): Post {
const post: Post = {
id: this.nextPostId++,
title,
content,
authorId,
published: false,
createdAt: new Date(),
};
this.posts.set(post.id, post);
return post;
}
getPost(id: number): Post | undefined {
return this.posts.get(id);
}
getPostsByAuthor(authorId: number): Post[] {
return Array.from(this.posts.values()).filter(p => p.authorId === authorId);
}
getPublishedPosts(limit = 20, offset = 0): Post[] {
return Array.from(this.posts.values())
.filter(p => p.published)
.slice(offset, offset + limit);
}
updatePost(id: number, data: Partial<Post>): Post | undefined {
const post = this.posts.get(id);
if (!post) return undefined;
Object.assign(post, data);
return post;
}
deletePost(id: number): boolean {
return this.posts.delete(id);
}
}
const db = new Database();
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-key-change-in-production';
// ============================================
// App Setup
// ============================================
const app = new Hono<{ Bindings: Env; Variables: Variables }>();
// Global Middleware
app.use('*', logger());
app.use('*', prettyJSON());
app.use('*', secureHeaders());
app.use('*', cors({
origin: ['http://localhost:3000', 'https://example.com'],
credentials: true,
}));
// Error Handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse();
}
console.error('Error:', err);
return c.json({ error: 'Internal Server Error' }, 500);
});
// ============================================
// Public Routes
// ============================================
app.get('/', (c) => {
return c.json({
name: 'Hono API Template',
version: '1.0.0',
endpoints: ['/auth', '/users', '/posts'],
});
});
app.get('/health', (c) => {
return c.json({
status: 'healthy',
timestamp: new Date().toISOString(),
memory: process.memoryUsage?.() ?? 'N/A',
});
});
// ============================================
// Auth Routes
// ============================================
const auth = new Hono<{ Bindings: Env; Variables: Variables }>();
auth.post('/register',
validator('json', (value, c) => {
const { email, name, password } = value as any;
if (!email || typeof email !== 'string' || !email.includes('@')) {
return c.json({ error: 'Invalid email' }, 400);
}
if (!name || typeof name !== 'string' || name.length < 2) {
return c.json({ error: 'Name must be at least 2 characters' }, 400);
}
if (!password || typeof password !== 'string' || password.length < 8) {
return c.json({ error: 'Password must be at least 8 characters' }, 400);
}
return { email, name, password };
}),
async (c) => {
const { email, name, password } = c.req.valid('json');
if (db.getUserByEmail(email)) {
throw new HTTPException(409, { message: 'Email already exists' });
}
const user = db.createUser(email, name, password);
const token = await sign({ id: user.id, email: user.email }, JWT_SECRET);
return c.json({ user, token }, 201);
}
);
auth.post('/login',
validator('json', (value, c) => {
const { email, password } = value as any;
if (!email || !password) {
return c.json({ error: 'Email and password required' }, 400);
}
return { email, password };
}),
async (c) => {
const { email, password } = c.req.valid('json');
const user = db.getUserByEmail(email);
if (!user || user.passwordHash !== `hashed-${password}`) {
throw new HTTPException(401, { message: 'Invalid credentials' });
}
const { passwordHash, ...publicUser } = user;
const token = await sign({ id: user.id, email: user.email }, JWT_SECRET);
return c.json({ user: publicUser, token });
}
);
app.route('/auth', auth);
// ============================================
// Protected Routes Middleware
// ============================================
const protected_ = new Hono<{ Bindings: Env; Variables: Variables }>();
protected_.use('*', jwt({ secret: JWT_SECRET }));
protected_.use('*', async (c, next) => {
const payload = c.get('jwtPayload') as { id: number; email: string };
c.set('user', payload);
await next();
});
// ============================================
// User Routes
// ============================================
const users = new Hono<{ Bindings: Env; Variables: Variables }>();
users.get('/', (c) => {
const users = db.getAllUsers();
return c.json({ users, count: users.length });
});
users.get('/me', (c) => {
const user = c.get('user');
if (!user) throw new HTTPException(401, { message: 'Not authenticated' });
const fullUser = db.getUser(user.id);
return c.json(fullUser);
});
users.get('/:id', (c) => {
const id = parseInt(c.req.param('id'));
const user = db.getUser(id);
if (!user) throw new HTTPException(404, { message: 'User not found' });
return c.json(user);
});
protected_.route('/users', users);
// ============================================
// Post Routes
// ============================================
const posts = new Hono<{ Bindings: Env; Variables: Variables }>();
posts.get('/', (c) => {
const limit = parseInt(c.req.query('limit') ?? '20');
const offset = parseInt(c.req.query('offset') ?? '0');
const posts = db.getPublishedPosts(limit, offset);
return c.json({ posts, count: posts.length });
});
posts.get('/my', (c) => {
const user = c.get('user');
if (!user) throw new HTTPException(401, { message: 'Not authenticated' });
const posts = db.getPostsByAuthor(user.id);
return c.json({ posts, count: posts.length });
});
posts.get('/:id', (c) => {
const id = parseInt(c.req.param('id'));
const post = db.getPost(id);
if (!post) throw new HTTPException(404, { message: 'Post not found' });
return c.json(post);
});
posts.post('/',
validator('json', (value, c) => {
const { title, content } = value as any;
if (!title || typeof title !== 'string' || title.length < 3) {
return c.json({ error: 'Title must be at least 3 characters' }, 400);
}
if (!content || typeof content !== 'string') {
return c.json({ error: 'Content is required' }, 400);
}
return { title, content };
}),
(c) => {
const user = c.get('user');
if (!user) throw new HTTPException(401, { message: 'Not authenticated' });
const { title, content } = c.req.valid('json');
const post = db.createPost(title, content, user.id);
return c.json(post, 201);
}
);
posts.patch('/:id/publish', (c) => {
const user = c.get('user');
if (!user) throw new HTTPException(401, { message: 'Not authenticated' });
const id = parseInt(c.req.param('id'));
const post = db.getPost(id);
if (!post) throw new HTTPException(404, { message: 'Post not found' });
if (post.authorId !== user.id) throw new HTTPException(403, { message: 'Not authorized' });
const updated = db.updatePost(id, { published: true });
return c.json(updated);
});
posts.delete('/:id', (c) => {
const user = c.get('user');
if (!user) throw new HTTPException(401, { message: 'Not authenticated' });
const id = parseInt(c.req.param('id'));
const post = db.getPost(id);
if (!post) throw new HTTPException(404, { message: 'Post not found' });
if (post.authorId !== user.id) throw new HTTPException(403, { message: 'Not authorized' });
db.deletePost(id);
return c.json({ deleted: true }, 200);
});
protected_.route('/posts', posts);
// Mount protected routes
app.route('/api', protected_);
// ============================================
// Export
// ============================================
export default app;
// For local development with Node.js
// import { serve } from '@hono/node-server';
// serve({ fetch: app.fetch, port: 3000 }, (info) => {
// console.log(`🚀 Hono Server: http://localhost:${info.port}`);
// });
// For Cloudflare Workers (default export works)
// For Bun: Bun.serve({ fetch: app.fetch, port: 3000 });
// For Deno: Deno.serve({ port: 3000 }, app.fetch);
export { app, db };