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 };