Skip to content

thanhhoajs/thanhhoa

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

65 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

ThanhHoa Logo

ThanhHoa β€” Ultra-High Performance Web Framework for Bun

Lightweight, blazing-fast web framework built for Bun. Two runtime dependencies. Zero compromise on speed.

npm version license

✨ Features

  • πŸš€ ~80,000+ req/s β€” 0.12ms average latency on Bun 1.3
  • πŸͺΆ 2 runtime deps β€” only radix3 + ajv; everything else is Bun-native
  • 🎯 Simple API β€” routing, middleware, groups, sub-routers
  • πŸ” Security built-in β€” JWT, CSRF, Basic/Bearer auth, helmet, CORS
  • ⚑ Brotli + gzip + deflate β€” auto-negotiated compression
  • πŸ—„οΈ Zero-dep LRU cache β€” internal O(1) cache, no npm package needed
  • πŸ”Œ Native WebSocket β€” per-route WebSocket handlers via Bun
  • πŸ§ͺ Test client β€” test routes without starting an HTTP server

πŸ“¦ Installation

bun add @thanhhoajs/thanhhoa

Requirements: Bun >= 1.2.0

πŸš€ Quick Start

import { ThanhHoa, json } from '@thanhhoajs/thanhhoa';

const app = new ThanhHoa();

app.get('/hello', () => json({ message: 'Hello World!' }));

app.listen({ port: 3000 });

πŸ“– Routing

HTTP Methods

import { ThanhHoa, json, text, html } from '@thanhhoajs/thanhhoa';

const app = new ThanhHoa();

app.get('/users', () => json({ users: [] }));
app.post('/users', async (ctx) => json(await ctx.request.json(), 201));
app.put('/users/:id', async (ctx) => json({ id: ctx.params.id }));
app.patch('/users/:id', async (ctx) => json({ updated: true }));
app.delete('/users/:id', (ctx) => json({ deleted: ctx.params.id }));
app.head('/ping', () => new Response(null, { status: 204 }));

app.listen({ port: 3000 });

Route Parameters & Query

// Dynamic params
app.get('/posts/:postId/comments/:commentId', (ctx) => {
  const { postId, commentId } = ctx.params;
  return json({ postId, commentId });
});

// Query string
app.get('/search', (ctx) => {
  const { q, page = '1', limit = '10' } = ctx.query;
  return json({ q, page, limit });
});

Route Groups

app.group('/api', (api) => {
  api.group('/v1', (v1) => {
    v1.get('/status', () => json({ version: '1.0' })); // GET /api/v1/status
    v1.get('/users', () => json({ users: [] }));        // GET /api/v1/users
  });
});

Sub-Routers

import { Router, ThanhHoa, json } from '@thanhhoajs/thanhhoa';

const userRouter = new Router();
userRouter.get('/', () => json({ users: [] }));
userRouter.get('/:id', (ctx) => json({ id: ctx.params.id }));
userRouter.post('/', () => json({ created: true }, 201));

const app = new ThanhHoa();
app.mount('/users', userRouter);
// Routes: GET /users, GET /users/:id, POST /users

app.listen({ port: 3000 });

πŸ› οΈ Response Helpers

import { json, text, html, redirect } from '@thanhhoajs/thanhhoa';

app.get('/json',     () => json({ data: 'value' }));
app.get('/text',     () => text('Hello World'));
app.get('/page',     () => html('<h1>Welcome</h1>'));
app.get('/old-path', () => redirect('/new-path'));
app.get('/created',  () => json({ id: 1 }, 201));

// BigInt is serialized safely
app.get('/bigint', () => json({ id: 9007199254740993n }));

πŸ”— Middleware

// Global middleware
app.use(async (ctx, next) => {
  const start = Date.now();
  const response = await next();
  console.log(`${ctx.request.method} ${ctx.request.url} β€” ${Date.now() - start}ms`);
  return response;
});

// Route-level middleware
const auth = async (ctx, next) => {
  if (!ctx.request.headers.get('Authorization')) {
    return json({ error: 'Unauthorized' }, 401);
  }
  return next();
};

app.get('/protected', (ctx) => json({ ok: true }), [auth]);

Context (ctx) API

ctx.request         // Native Fetch API Request
ctx.params          // Route params: { id: '42' }
ctx.query           // Query string: { q: 'search' }
ctx.cookies         // Cookie interface (get/set/delete)
ctx.socketAddress   // Client IP info
ctx.state           // Typed app-wide state
ctx.locals          // Request-scoped Map (ctx.set / ctx.get)
ctx.validatedBody   // Body after validate() middleware
ctx.jwtPayload      // JWT payload after jwt() middleware
ctx.csrfToken       // CSRF token after csrf() middleware

// Shortcut body parsers
await ctx.json<T>()
await ctx.text()
await ctx.formData()
ctx.stream()

// Resource preloading (Link header)
ctx.preload('/style.css', 'style');
ctx.preload('/app.js', 'script');

πŸ” Security Middleware

JWT Authentication

import { jwt, signToken, verifyToken, decodeToken } from '@thanhhoajs/thanhhoa';

const token = await signToken({ userId: 1 }, 'your-secret', { expiresIn: 3600 });

app.use(jwt({
  secret: 'your-secret',
  skip: ['/login', '/register']
}));

app.get('/profile', (ctx) => json({ user: ctx.jwtPayload }));

const payload = decodeToken(token); // Decode without verification

CSRF Protection

import { csrf } from '@thanhhoajs/thanhhoa';

app.use(csrf());

app.get('/form', (ctx) => html(`
  <form method="POST" action="/submit">
    <input type="hidden" name="_csrf" value="${ctx.csrfToken}">
    <button>Submit</button>
  </form>
`));
// AJAX: send X-CSRF-Token header with the token value

Basic Auth

import { basicAuth } from '@thanhhoajs/thanhhoa';

app.use(basicAuth({ username: 'admin', password: 'secret' }));

// Custom validation
app.use(basicAuth({
  username: '',
  password: '',
  validate: async (user, pass) => db.verifyUser(user, pass)
}));

Bearer Token Auth

import { bearerAuth } from '@thanhhoajs/thanhhoa';

app.use(bearerAuth({ token: process.env.API_KEY! }));

// Dynamic validation
app.use(bearerAuth({
  validate: async (token) => db.isValidApiKey(token)
}));

Security Headers (Helmet)

import { helmet } from '@thanhhoajs/thanhhoa';

app.use(helmet());
app.use(helmet({ contentSecurityPolicy: false }));

CORS

import { cors } from '@thanhhoajs/thanhhoa';

app.use(cors({
  origin: 'https://example.com',
  methods: ['GET', 'POST'],
  credentials: true
}));

⚑ Performance Middleware

Compression (Brotli / Gzip / Deflate)

Auto-negotiated via Accept-Encoding. Brotli (br) is preferred when available.

import { compress } from '@thanhhoajs/thanhhoa';

app.use(compress()); // br > gzip > deflate (default)

app.use(compress({
  threshold: 1024,           // Only compress > 1KB
  encoding: ['gzip'],        // Force gzip only
}));

Cache-Control

import {
  cacheControl,
  noStore,
  publicCache,
  privateCache
} from '@thanhhoajs/thanhhoa';

// API routes β€” no caching
app.group('/api', (r) => { /* ... */ }, [noStore()]);

// Public CDN assets β€” 1 hour, stale-while-revalidate
app.use(publicCache(3600, { staleWhileRevalidate: 60 }));

// Per-user content β€” browser cache only, 5 min
app.use(privateCache(300));

// Versioned static assets β€” immutable, 1 year
app.use(publicCache(31536000, { immutable: true }));

// Full control
app.use(cacheControl({
  maxAge: 3600,
  staleIfError: 86400,
  mustRevalidate: true,
  skip: ['/health']
}));

Rate Limiting

import { rateLimit, MemoryStore } from '@thanhhoajs/thanhhoa';

// In-memory (single instance)
app.use(rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  message: 'Too many requests'
}));

// Redis (distributed)
import { RedisClient } from 'bun';
import { RedisStore } from '@thanhhoajs/thanhhoa';

const redis = new RedisClient('redis://localhost:6379');
app.use(rateLimit({ store: new RedisStore(redis), windowMs: 60000, max: 100 }));

// Custom key (by user ID instead of IP)
app.use(rateLimit({
  keyGenerator: (req) => req.headers.get('X-User-ID') ?? 'anon',
  max: 1000
}));

// Skip paths
app.use(rateLimit({ max: 100, skip: ['/health', '/metrics'] }));

Sliding Window Rate Limit (Redis)

import { slidingWindowRateLimit } from '@thanhhoajs/thanhhoa';

app.use(slidingWindowRateLimit({ redis, max: 100, windowMs: 60000 }));

ETag (HTTP Caching)

import { etag } from '@thanhhoajs/thanhhoa';

app.use(etag());                              // Weak ETag, FNV-1a
app.use(etag({ weak: false, algorithm: 'sha256' })); // Strong ETag

πŸ›‘οΈ Request Middleware

Body Size Limit

import { bodyLimit } from '@thanhhoajs/thanhhoa';

app.use(bodyLimit({ maxSize: 1024 * 1024 })); // 1 MB

Request Timeout

import { timeout } from '@thanhhoajs/thanhhoa';

app.use(timeout({ ms: 30000, message: 'Request timed out' }));

Request ID

Stored in ctx.get('requestId') and X-Request-ID response header.

import { requestId } from '@thanhhoajs/thanhhoa';

app.use(requestId());
app.use(requestId({ header: 'X-Trace-ID' }));

Logger

import { logger } from '@thanhhoajs/thanhhoa';

app.use(logger());
// [THANHHOA] GET /api/users 200 β€” 2.41ms

βœ… Validation (ajv)

import { validate, validateQuery, validateParams } from '@thanhhoajs/thanhhoa';

const userSchema = {
  type: 'object',
  properties: {
    name: { type: 'string', minLength: 1 },
    email: { type: 'string', format: 'email' }
  },
  required: ['name', 'email']
};

app.post('/users', async (ctx) => {
  const user = ctx.validatedBody; // Already validated and typed
  return json({ created: user }, 201);
}, [validate(userSchema)]);

// Query params
app.get('/search', handler, [validateQuery({ type: 'object', properties: { q: { type: 'string' } } })]);

πŸͺ Cookies

app.get('/profile', (ctx) => {
  const session = ctx.cookies.get('session');
  return json({ session });
});

app.post('/login', (ctx) => {
  ctx.cookies.set('session', 'token123', {
    httpOnly: true,
    secure: true,
    sameSite: 'Strict',
    maxAge: 3600
  });
  return json({ success: true });
});

app.post('/logout', (ctx) => {
  ctx.cookies.delete('session');
  return json({ loggedOut: true });
});

πŸ“ Static Files & SPA

const app = new ThanhHoa({
  staticDirectories: [
    { path: '/static', directory: 'public' },
    { path: '/assets', directory: 'assets' }
  ],
  spa: true // Serve index.html for unknown routes
});

πŸ”Œ WebSocket

app.ws('/chat', {
  open(ws) {
    console.log('Connected:', ws.data.id);
    ws.subscribe('room:1');
  },
  message(ws, msg) {
    ws.publish('room:1', msg);
  },
  close(ws, code, reason) {
    console.log('Disconnected');
  }
});

πŸ“‚ File Responses

import { file, attachment, inline, blob } from '@thanhhoajs/thanhhoa';

app.get('/pdf/:id',  (ctx) => file(`./docs/${ctx.params.id}.pdf`));
app.get('/export',   () => attachment('./data.csv', 'report.csv'));
app.get('/preview',  () => inline('./document.pdf'));
app.get('/image',    () => blob(generateBuffer(), 'image/png'));

πŸ“€ File Upload

import { uploadFile } from '@thanhhoajs/thanhhoa';

app.post('/upload', async (ctx) => {
  const formData = await ctx.formData();
  const file = formData.get('file');

  if (!(file instanceof File)) return json({ error: 'No file' }, 400);

  const result = await uploadFile(file, {
    directory: 'uploads',
    allowedTypes: ['image/png', 'image/jpeg'],
    maxSize: 5 * 1024 * 1024 // 5 MB
  });

  return json(result);
});

🌊 Streams & SSE

import { streamResponse, createDirectStream } from '@thanhhoajs/thanhhoa';

// Server-Sent Events
app.get('/events', () =>
  streamResponse(async function* () {
    yield 'data: connected\n\n';
    for (let i = 0; i < 10; i++) {
      await Bun.sleep(1000);
      yield `data: tick ${i}\n\n`;
    }
  }(), { headers: { 'Content-Type': 'text/event-stream' } })
);

// Zero-copy direct stream
app.get('/stream', () => {
  const body = createDirectStream(async (ctrl) => {
    ctrl.write('hello ');
    ctrl.write('world');
  });
  return new Response(body);
});

πŸ—„οΈ Session Management

import { sessionManager, MemorySessionStore, RedisSessionStore } from '@thanhhoajs/thanhhoa';
import { RedisClient } from 'bun';

// Development: in-memory
const session = sessionManager(new MemorySessionStore());

// Production: Redis
const session = sessionManager(new RedisSessionStore(new RedisClient()), {
  ttl: 86400 // 24 hours
});

app.post('/login', async (ctx) => {
  const id = await session.create({ userId: 1, role: 'admin' });
  ctx.cookies.set('session', id, { httpOnly: true, secure: true });
  return json({ success: true });
});

app.get('/me', async (ctx) => {
  const data = await session.get(ctx.cookies.get('session')!);
  if (!data) return json({ error: 'Not logged in' }, 401);
  return json(data);
});

app.post('/logout', async (ctx) => {
  await session.destroy(ctx.cookies.get('session')!);
  ctx.cookies.delete('session');
  return json({ success: true });
});

πŸ§ͺ Testing (No HTTP server needed)

import { testClient } from '@thanhhoajs/thanhhoa';

const app = new ThanhHoa();
app.get('/hello', () => json({ message: 'Hello!' }));

const client = testClient(app);

const res = await client.get('/hello');
console.log(await res.json()); // { message: 'Hello!' }

const post = await client.post('/users', {
  body: { name: 'John' },
  headers: { Authorization: 'Bearer token' }
});

πŸ”Œ App State

interface AppState {
  db: Database;
  config: Config;
}

const app = new ThanhHoa<AppState>();
app.state.db = dbConnection;

app.get('/users', (ctx) => {
  const users = ctx.state.db.query('SELECT * FROM users');
  return json({ users });
});

πŸš€ Starting the Server

// Basic
app.listen({ port: 3000 });

// With hostname
app.listen({ port: 3000, hostname: '0.0.0.0' });

// HTTPS (TLS)
app.listen({
  port: 443,
  tls: {
    key: Bun.file('./server.key'),
    cert: Bun.file('./server.cert')
  }
});

πŸ—„οΈ LRU Cache Utility

Built-in O(1) LRU cache β€” no npm dependency required.

import { LRUCache } from '@thanhhoajs/thanhhoa';

const cache = new LRUCache<string, User>({
  max: 1000,
  ttl: 60_000,        // ms, optional
  updateAgeOnGet: true
});

cache.set('user:1', userData);
const user = cache.get('user:1'); // undefined if expired or evicted
cache.has('user:1');
cache.delete('user:1');
cache.clear();

πŸ”΄ Error Handling

import { HttpException } from '@thanhhoajs/thanhhoa';

// Throw from anywhere β€” returns correct status automatically
app.get('/item/:id', (ctx) => {
  throw new HttpException('Not found', 404);
});

// Custom global error handler
app.setErrorHandler({
  handle(error, ctx) {
    if (error instanceof HttpException) return error.toResponse();
    return json({ error: 'Internal Server Error' }, 500);
  }
});

πŸ“Š Benchmark Results

Environment: Windows 11, Bun 1.3.8 Config: 5,000 iterations Γ— 2 requests per iteration

Metric Result
Average Latency 0.12ms
Throughput ~80,000+ req/s
Framework Estimated RPS
Express.js ~13,000
Fastify ~30,000
Hono ~60,000
ElysiaJS ~70,000
ThanhHoaJS ~80,000+

Set NODE_ENV=production for maximum performance.


πŸ“„ License

MIT Β© Nguyen Nhu Khanh

About

ThanhHoa is a lightweight, high-performance web framework for Bun, designed to make server-side development simple and enjoyable.

Topics

Resources

License

Stars

16 stars

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors