
Bunshine
A Bun HTTP & WebSocket server that is a little ray of sunshine.
bun install bunshine
Motivation
- Use bare
Request
and Response
objects - Support for routing
WebSocket
requests - Support for Server Sent Events
- Support ranged file downloads (e.g. for video streaming)
- Be very lightweight
- Treat every handler function like middleware
- Support async handlers
- Provide common middleware out of the box
- Make specifically for Bun
- Comprehensive unit tests
Table of Contents
- Basic example
- Full example
- Serving static files
- Middleware
- WebSockets
- WebSocket pub-sub
- Server Sent Events
- Routing examples
- Middleware
- Roadmap
- License
Usage
Basic example
import Router from 'bunshine';
const app = new Router();
app.get('/', () => {
return new Response('Hello World!');
});
app.listen({ port: 3100 });
Full example
import Router, { json, redirect } from 'bunshine';
const app = new Router();
app.patch('/users/:id', async ({ request, params, url }) => {
await authorize(request.headers.get('Authorization'));
const data = await request.json();
const result = await updateUser(params.id, data);
if (result === 'not found') {
return json({ error: 'User not found' }, { status: 404 });
} else if (result === 'error') {
return json({ error: 'Error updating user' }, { status: 500 });
} else {
return json({ error: false });
}
});
app.on404(c => {
console.log('404');
return c.json({ error: 'Not found' }, { status: 404 });
});
app.on500(c => {
console.log('500');
return c.json({ error: 'Internal server error' }, { status: 500 });
});
app.listen({ port: 3100 });
function authorize(authHeader: string) {
if (!authHeader) {
throw redirect('/login');
} else if (!jwtVerify(authHeader)) {
throw redirect('/not-allowed');
}
}
What is c
here?
What does it mean that "every handler is treated like middleware"?
Serving static files
import Router, { serveFiles } from 'bunshine';
const app = new Router();
app.use(serveFiles(`${import.meta.dir}/public`));
app.listen({ port: 3100 });
Middleware
import Router from 'bunshine';
const app = new Router();
app.use(c => {
if (!isAllowed(c.request.headers.get('Authorization'))) {
return c.redirect('/login', { status: 403 });
}
});
app.use(async (c, next) => {
const resp = await next();
if (resp.status === 403) {
logThatUserWasForbidden(c.request.url);
}
return resp;
});
app.use(async (c, next) => {
logRequest(c.request);
const resp = await next();
logResponse(resp);
return resp;
});
app.get('/admin', c => {
if (!isAdmin(c.request.headers.get('Authorization'))) {
return c.redirect('/login', { status: 403 });
}
});
app.get('/users/:id', [
paramValidationMiddleware,
async c => {
const user = await getUser(c.params.id);
return c.json(user);
},
]);
app.get('/users/:id', paramValidationMiddleware, async c => {
const user = await getUser(c.params.id);
return c.json(user);
});
app.get('/', c => c.text('Hello World!'));
app.listen({ port: 3100 });
Note that because every handler is treated like middleware,
you must register handlers in order of specificity. For example:
app.get('/users/me', handler1);
app.get('/users/:id', handler2);
WebSockets
import Router from 'bunshine';
const app = new Router();
app.get('/', c => c.text('Hello World!'));
app.socket.at<{ user: string }>('/games/rooms/:room', {
upgrade: ({ request, params, url }) => {
const cookies = req.headers.get('cookie');
const user = getUserFromCookies(cookies);
return { user };
},
error: (ws, error) => {
console.log('WebSocket error', error);
},
open(ws) {
const room = ws.data.params.room;
const user = ws.data.user;
markUserEntrance(room, user);
ws.send(getGameState(room));
},
message(ws, message) {
const room = ws.data.params.room;
const user = ws.data.user;
const result = saveMove(room, user, message);
ws.send(result);
},
close(ws, code, message) {
const room = ws.data.params.room;
const user = ws.data.user;
markUserExit(room, user);
},
});
app.listen({ port: 3100 });
const gameRoom = new WebSocket('ws://localhost:3100/games/rooms/1?user=42');
gameRoom.onmessage = e => {
const data = JSON.parse(e.data);
if (data.type === 'GameState') {
setGameState(data);
} else if (data.type === 'GameMove') {
playMove(data);
}
};
gameRoom.onerror = handleGameError;
gameRoom.send(JSON.stringify({ type: 'GameMove', move: 'rock' }));
WebSocket pub-sub
import Router from 'bunshine';
const app = new Router();
app.get('/', c => c.text('Hello World!'));
app.socket.at('/chat/:room', {
upgrade: ({ request, params, url }) => {
const cookies = request.headers.get('cookie');
const username = getUsernameFromCookies(cookies);
return { username };
},
open(ws) {
const msg = `${ws.data.username} has entered the chat`;
ws.subscribe('the-group-chat');
ws.publish('the-group-chat', msg);
},
message(ws, message) {
ws.publish('the-group-chat', `${ws.data.username}: ${message}`);
},
close(ws, code, message) {
const msg = `${ws.data.username} has left the chat`;
ws.publish('the-group-chat', msg);
ws.unsubscribe('the-group-chat');
},
});
Server Sent Events
import Router from 'bunshine';
const app = new Router();
app.get('/stock/:symbol', c => {
const symbol = c.params.symbol;
return c.sse(send => {
setInterval(async () => {
const data = getPriceData(symbol);
send('price', { gain: data.gain, price: data.price });
}, 6000);
});
});
app.listen({ port: 3100 });
const livePrice = new EventSource('http://localhost:3100/stock/GOOG');
livePrice.addEventListener('price', e => {
const { gain, price } = JSON.parse(e.data);
document.querySelector('#stock-GOOG-gain').innerText = gain;
document.querySelector('#stock-GOOG-price').innerText = price;
});
Routing examples
Bunshine uses the path-to-regexp
package for processing path routes. For more
info, checkout the path-to-regexp docs.
Path | URL | params |
---|
'/path' | '/path' | {} |
'/users/:id' | '/users/123' | { id: '123' } |
'/users/:id/groups' | '/users/123/groups' | { id: '123' } |
'/u/:id/groups/:gid' | '/u/1/groups/a' | { id: '1', gid: 'a' } |
'/star/*' | '/star/man' | { 0: 'man' } |
'/star/*/can' | '/star/man/can' | { 0: 'man' } |
'/users/(\\d+)' | '/users/123' | { 0: '123' } |
/users/(\d+)/ | '/users/123' | { 0: '123' } |
/users/([a-z-]+)/ | '/users/abc-def' | { 0: 'abc-def' } |
'/(users|u)/:id' | '/users/123' | { id: '123' } |
'/(users|u)/:id' | '/u/123' | { id: '123' } |
'/:a/:b?' | '/123' | { a: '123' } |
'/:a/:b?' | '/123/abc' | { a: '123', b: 'abc' } |
Middleware
serveFiles
Serve static files from a directory.
import Router, { serveFiles } from 'bunshine';
const app = new Router();
app.get('/public', serveFiles(`${import.meta.dir}/public`));
app.listen({ port: 3100 });
cors
devLogger
performanceLogger
prodLogger
Roadmap
- ✅ HttpRouter
- ✅ SocketRouter
- ✅ Context
- ✅ middleware > serveFiles
- ✅ middleware > cors
- ✅ middleware > devLogger
- ✅ middleware > prodLogger
- 🔲 middleware > performanceLogger
- 🔲 middleware > securityHeaders
- 🔲 examples/server.ts
- 🔲 GitHub Actions to run tests and coverage
License
ISC License