Migrating from Express
Shokupan is designed to feel familiar to Express developers, but with modern enhancements like built-in TypeScript support, a unified context object, and standard Web API usage.
Key Differences
Section titled “Key Differences”- Context vs Req/Res: Instead of separate
reqandresobjects, Shokupan passes a singlectx(context) object. - Return vs Send: Handlers return data directly (JSON, string, Response) or call
ctx.send(),ctx.json(),ctx.text(), etc. instead of callingres.json(). - Async Middleware: Middleware can
await next()to easily run code after downstream handlers. - Web Standards: Uses standard
Request,Response, andURLobjects.
Basic Server
Section titled “Basic Server”Express:
import express from 'express';const app = express();
app.get('/', (req, res) => { res.json({ message: 'Hello' });});
app.listen(3000);Shokupan:
import { Shokupan } from 'shokupan';const app = new Shokupan({ port: 3000 });
app.get('/', (ctx) => { // Return data directly or call ctx.send(), ctx.json(), ctx.text(), etc. return { message: 'Hello' };});
app.listen();Middleware
Section titled “Middleware”Middleware in Shokupan can optionally await next() to run code after downstream handlers complete.
Express:
app.use((req, res, next) => { console.log(req.method, req.path); req.on("finish", () => { console.log(`Request finished with status ${res.statusCode}`); }); next();});Shokupan:
app.use(async (ctx, next) => { console.log(ctx.method, ctx.path);
// Optional: await next() to run code after handler await next();
// Post-processing happens after downstream completes console.log(`Request finished with status ${ctx.response.status}`);});Async Error Handling
Section titled “Async Error Handling”One of the biggest pain points in Express is that async route handlers require manual try/catch wrappers to handle errors. Shokupan handles this automatically.
Express (Manual Error Handling Required):
// ❌ This will throw an unhandled error if fetchUser throws -- leading to requests that never complete.app.get('/users/:id', async (req, res) => { const user = await fetchUser(req.params.id); res.json(user);});
// ✅ Express requires manual try/catchapp.get('/users/:id', async (req, res, next) => { try { const user = await fetchUser(req.params.id); res.json(user); } catch (error) { next(error); // Pass to error handler }});Shokupan (Automatic Error Handling):
// ✅ async errors are automatically caught and handledapp.get('/users/:id', async (ctx) => { const user = await fetchUser(ctx.params.id); return user;});
// Optional: Add global error handling middleware for custom error responsesapp.use(async (ctx, next) => { try { await next(); } catch (error) { return ctx.json({ error: error.message }, 500); }});
// OR use an error hookapp.onError((ctx, error) => { return ctx.json({ error: error.message }, 500);});Request & Response
Section titled “Request & Response”| Feature | Express | Shokupan |
|---|---|---|
| Path | req.path | ctx.path |
| Method | req.method | ctx.method |
| Query | req.query['id'] | ctx.query.get('id') |
| Headers | req.headers['auth'] | ctx.headers.get('auth') |
| Body | req.body | ctx.body (typed) |
| Status | res.status(404) | return ctx.text('Not Found', 404) |
| Send JSON | res.json(data) | return data or return ctx.json(data) |
Streaming
Section titled “Streaming”Express uses res.write() and res.pipe() for streaming. Shokupan provides modern streaming helpers.
Express (Manual Streaming):
app.get('/stream', (req, res) => { res.setHeader('Content-Type', 'text/plain'); res.write('Hello '); res.write('World'); res.end();});
// Pipingapp.get('/file', (req, res) => { const stream = fs.createReadStream('./file.txt'); stream.pipe(res);});Shokupan (Built-in Streaming):
app.get('/stream', (ctx) => { return ctx.streamText(async (stream) => { await stream.write('Hello '); await stream.write('World'); });});
// Pipingapp.get('/file', async (ctx) => { const file = Bun.file('./file.txt'); return ctx.pipe(file.stream());});Server-Sent Events:
Express requires manual SSE formatting:
app.get('/events', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache');
setInterval(() => { res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`); }, 1000);});Shokupan has built-in SSE support:
app.get('/events', (ctx) => { return ctx.streamSSE(async (stream) => { while (true) { await stream.writeSSE({ data: JSON.stringify({ time: Date.now() }) }); await stream.sleep(1000); } });});Migration Checklist
Section titled “Migration Checklist”Next Steps
Section titled “Next Steps”- Routing - Learn Shokupan patterns
- Controllers - Use class-based controllers