Skip to main content
Glama
api.ts10.7 kB
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 };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/millsydotdev/Code-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server