Skip to content

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.

  1. Context vs Req/Res: Instead of separate req and res objects, Shokupan passes a single ctx (context) object.
  2. Return vs Send: Handlers return data directly (JSON, string, Response) or call ctx.send(), ctx.json(), ctx.text(), etc. instead of calling res.json().
  3. Async Middleware: Middleware can await next() to easily run code after downstream handlers.
  4. Web Standards: Uses standard Request, Response, and URL objects.

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

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/catch
app.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 handled
app.get('/users/:id', async (ctx) => {
const user = await fetchUser(ctx.params.id);
return user;
});
// Optional: Add global error handling middleware for custom error responses
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
return ctx.json({ error: error.message }, 500);
}
});
// OR use an error hook
app.onError((ctx, error) => {
return ctx.json({ error: error.message }, 500);
});
FeatureExpressShokupan
Pathreq.pathctx.path
Methodreq.methodctx.method
Queryreq.query['id']ctx.query.get('id')
Headersreq.headers['auth']ctx.headers.get('auth')
Bodyreq.bodyctx.body (typed)
Statusres.status(404)return ctx.text('Not Found', 404)
Send JSONres.json(data)return data or return ctx.json(data)

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();
});
// Piping
app.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');
});
});
// Piping
app.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);
}
});
});