bunshine
v1.0.0
Published
A Bun HTTP & WebSocket server that is a little ray of sunshine.
Downloads
179
Maintainers
Readme
Bunshine
A Bun HTTP & WebSocket server that is a little ray of sunshine.
Installation
bun add bunshine
Or to run Bunshine on Node, install Nodeshine.
Motivation
- Use bare
Request
andResponse
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 like middleware
- Support async handlers
- Provide common middleware out of the box
- Built-in gzip compression
- Make specifically for Bun
- Comprehensive unit tests
- Support for
X-HTTP-Method-Override
header
Table of Contents
- Basic example
- Full example
- Serving static files
- Writing middleware
- Throwing responses
- WebSockets
- WebSocket pub-sub
- Server Sent Events
- Routing examples
- Included middleware
- TypeScript pro-tips
- Roadmap
- License
Usage
Basic example
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
app.get('/', c => {
return new Response('Hello at ' + c.url.pathname);
});
app.listen({ port: 3100 });
Full example
import { HttpRouter, redirect } from 'bunshine';
const app = new HttpRouter();
app.patch('/users/:id', async c => {
await authorize(c.request.headers.get('Authorization'));
const data = await c.request.json();
const result = await updateUser(params.id, data);
if (result === 'not found') {
return c.json({ error: 'User not found' }, { status: 404 });
} else if (result === 'error') {
return c.json({ error: 'Error updating user' }, { status: 500 });
} else {
return c.json({ error: false });
}
});
app.on404(c => {
// called when no handlers match the requested path
return c.text('Page Not found', { status: 404 });
});
app.on500(c => {
// called when a handler throws an error
console.error('500', c.error);
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?
c
is a Context
object that contains the request and params.
import { HttpRouter, type Context, type NextFunction } from 'bunshine';
const app = new HttpRouter();
app.get('/hello', (c: Context, next: NextFunction) => {
// Properties of the Context object
c.request; // The raw request object
c.url; // The URL object
c.params; // The request params from route placeholders
c.server; // The Bun server instance (useful for pub-sub)
c.app; // The HttpRouter instance
c.locals; // A place to persist data between handlers for the duration of the request
c.error; // An error object available to handlers registered with app.on500()
c.ip; // The IP address of the client (not necessarily the end user)
c.date; // The date of the request
c.now; // The result of performance.now() at the start of the request
// Convenience methods for creating Response objects with various content types
// Note that responses are automatically gzipped if the client accepts gzip
c.json(data, init);
c.text(text, init);
c.js(jsText, init);
c.xml(xmlText, init);
c.html(htmlText, init);
c.css(cssText, init);
c.file(path, init);
// Create a redirect Response
c.redirect(url, status);
});
Serving static files
Serving static files is easy with the serveFiles
middleware. Note that ranged
requests are supported, so you can use this for video streaming or partial
downloads.
import { HttpRouter, serveFiles } from 'bunshine';
const app = new HttpRouter();
app.get('/public/*', serveFiles(`${import.meta.dir}/public`));
app.listen({ port: 3100 });
See the serveFiles section for more info.
Also note you can serve files with bunshine anywhere with bunx bunshine serve
.
It currently uses the default serveFiles()
options.
Writing middleware
Here are more examples of attaching middleware.
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
// Run before each request
app.use(c => {
if (!isAllowed(c.request.headers.get('Authorization'))) {
// redirect instead of running other middleware or handlers
return c.redirect('/login', { status: 403 });
}
// continue to other handlers
});
// Run after each request
app.use(async (c, next) => {
// wait for response from other handlers
const resp = await next();
// peek at status and log if 403
if (resp.status === 403) {
logThatUserWasForbidden(c.request.url);
}
// return the response from the other handlers
return resp;
});
// Run before AND after each request
app.use(async (c, next) => {
logRequest(c.request);
const resp = await next();
logResponse(resp);
return resp;
});
// Middleware at a certain path
app.get('/admin', c => {
if (!isAdmin(c.request.headers.get('Authorization'))) {
return c.redirect('/login', { status: 403 });
}
});
// Middleware before a given handler (as array)
app.get('/users/:id', [
paramValidationMiddleware({ id: zod.number() }),
async c => {
const user = await getUser(c.params.id);
return c.json(user);
},
]);
// Middleware before a given handler (as args)
app.get('/users/:id', paramValidationMiddleware, async c => {
const user = await getUser(c.params.id);
return c.json(user);
});
// handler affected by applicable middleware
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 desired specificity. For example:
// This order matters
app.get('/users/me', handler1);
app.get('/users/:id', handler2);
app.get('*', http404Handler);
What does it mean that "every handler is treated like middleware"?
If a handler does not return a Response
object or return a promise that does
not resolve to a Response
object, then the next matching handler will be
called. Consider the following:
import { HttpRouter, type Context, type NextFunction } from 'bunshine';
const app = new HttpRouter();
// ❌ Incorrect asynchronous handler
app.get('/hello', (c: Context, next: NextFunction) => {
setTimeout(() => {
next(new Response('Hello World!'));
}, 1000);
});
// ✅ Correct asynchronous handler
app.get('/hello', async (c: Context) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(new Response('Hello World!'));
}, 1000);
});
});
It also means that the next()
function is async. Consider the following:
import { HttpRouter, type Context, type NextFunction } from 'bunshine';
const app = new HttpRouter();
// ❌ Incorrect use of next
app.get('/hello', (c: Context, next: NextFunction) => {
const resp = next();
});
// ✅ Correct use of next
app.get('/hello', async (c: Context, next: NextFunction) => {
// wait for other handlers to return a response
const resp = await next();
// do stuff with response
});
And finally, it means that .use()
is just a convenience function for
registering middleware. Consider the following:
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
// The following 2 are the same
app.use(middlewareHandler);
app.all('*', middlewareHandler);
This all-handlers-are-middleware behavior complements the way that handlers and middleware can be registered. Consider the following:
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
// middleware can be inserted with parameters
app.get('/admin', getAuthMiddleware('admin'), middleware2, handler);
// Bunshine accepts any number of middleware functions in parameters or arrays
// so the following are equivalent
app.get('/posts', middleware1, middleware2, handler);
app.get('/users', [middleware1, middleware2, handler]);
app.get('/visitors', [[middleware1, [middleware2, handler]]]);
Throwing responses
You can throw a Response
object from anywhere in your code to send a response.
Here is an example:
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
async function checkPermission(request: Request, action: string) {
const authHeader = request.headers.get('Authorization');
if (!(await hasPermission(authHeader, action))) {
throw c.redirect('/home');
} else if (hasTooManyRequests(authHeader)) {
throw c.json({ error: 'Too many requests' }, { status: 429 });
}
}
app.post('/posts', async c => {
await checkPermissions(c.request, 'create-post');
// code here will only run if checkPermission hasn't thrown a Response
});
// start the server
app.listen({ port: 3100 });
WebSockets
Setting up websockets at various paths is easy with the socket
property.
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
// regular routes
app.get('/', c => c.text('Hello World!'));
// WebSocket routes
type ParamsShape = { room: string };
type DataShape = { user: User };
app.socket.at<ParmasShape, DataShape>('/games/rooms/:room', {
// Optional. Allows you to specify arbitrary data to attach to ws.data.
upgrade: sc => {
const cookies = sc.request.headers.get('cookie');
const user = getUserFromCookies(cookies);
return { user };
},
// Optional. Allows you to deal with errors thrown by handlers.
error: (sc, error) => {
console.log('WebSocket error', error.message);
},
// Optional. Called when the client connects
open(sc) {
const room = sc.params.room;
const user = sc.data.user;
markUserEntrance(room, user);
ws.send(getGameState(room));
},
// Optional. Called when the client sends a message
message(sc, message) {
const room = sc.params.room;
const user = sc.data.user;
const result = saveMove(room, user, message.json());
// send accepts strings, Buffers, ArrayBuffers
// and anything else will be serialized to JSON
ws.send(result);
},
// Optional. Called when the client disconnects
// List of codes and messages: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
close(sc, code, message) {
const room = sc.params.room;
const user = sc.data.user;
markUserExit(room, user);
},
});
// start the server
app.listen({ port: 3100 });
//
// Browser side:
//
const gameRoom = new WebSocket('ws://localhost:3100/games/rooms/1?user=42');
gameRoom.onmessage = e => {
// receiving messages
const data = JSON.parse(e.data);
if (data.type === 'GameState') {
setGameState(data);
} else if (data.type === 'GameMove') {
playMove(data);
}
};
gameRoom.onerror = handleGameError;
// send message to server
gameRoom.send(JSON.stringify({ type: 'GameMove', move: 'rock' }));
WebSocket pub-sub
And WebSockets make it super easy to create a pub-sub system with no external dependencies.
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
app.get('/', c => c.text('Hello World!'));
type ParamsShape = { room: string };
type DataShape = { username: string };
app.socket.at<ParamsShape, DataShape>('/chat/:room', {
upgrade: c => {
const cookies = c.request.headers.get('cookie');
const username = getUsernameFromCookies(cookies);
return { username };
},
open(sc) {
const msg = `${sc.data.username} has entered the chat`;
sc.subscribe(`chat-room-${sc.params.room}`);
sc.publish(`chat-room-${sc.params.room}`, msg);
},
message(sc, message) {
// the server re-broadcasts incoming messages
// to each connection's message handler
const fullMessage = `${sc.data.username}: ${message}`;
sc.publish(`chat-room-${sc.params.room}`, fullMessage);
sc.send(fullMessage);
},
close(sc, code, message) {
const msg = `${sc.data.username} has left the chat`;
ws.publish(`chat-room-${sc.params.room}`, msg);
ws.unsubscribe(`chat-room-${sc.params.room}`);
},
});
const server = app.listen({ port: 3100 });
// at a later time, you can also publish a message from another source
server.publish(channel, message);
Server-Sent Events
Server-Sent Events (SSE) are similar to WebSockets, but one way. The server can send messages, but the client cannot. This is useful for streaming data to the browser.
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
app.get<{ symbol: string }>('/stock/:symbol', c => {
const symbol = c.params.symbol;
return c.sse(send => {
setInterval(async () => {
const data = await getPriceData(symbol);
send('price', { gain: data.gain, price: data.price });
}, 6000);
});
});
// start the server
app.listen({ port: 3100 });
//
// Browser side:
//
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;
});
Note that with SSE, the client must ultimately decide when to stop listening.
Creating an EventSource
object will open a connection to the server, and if
the server closes the connection, the browser will automatically reconnect.
So if you want to tell the browser you are done sending events, send a message that your client-side code will understand to mean "stop listening". Here is an example:
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
app.get<{ videoId: string }>('/convert-video/:videoId', c => {
const { videoId } = c.params;
return c.sse(send => {
const onProgress = percent => {
send('progress', { percent });
};
const onComplete = () => {
send('progress', { percent: 100 });
};
startVideoConversion(videoId, onProgress, onComplete);
});
});
// start the server
app.listen({ port: 3100 });
//
// Browser side:
//
const conversionProgress = new EventSource('/convert-video/123');
conversionProgress.addEventListener('progress', e => {
const data = JSON.parse(e.data);
if (data.percent === 100) {
conversionProgress.close();
} else {
document.querySelector('#progress').innerText = e.data;
}
});
You may have noticed that you can attach multiple listeners to an EventSource
object to react to multiple event types. Here is a minimal example:
//
// Server side
//
app.get('/hello', c => {
const { videoId } = c.params;
return c.sse(send => {
send('event1', 'data1');
send('event2', 'data2');
});
});
//
// Browser side:
//
const events = new EventSource('/hello');
events.addEventListener('event1', listener1);
events.addEventListener('event2', listener2);
Routing examples
Bunshine uses the path-to-regexp
package for processing path routes. For more
info, checkout the path-to-regexp docs.
Path examples
| 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' }
|
HTTP methods
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
app.head('/posts/:id', doesPostExist);
app.get('/posts/:id', getPost);
app.post('/posts/:id', addPost);
app.patch('/posts/:id', editPost);
app.put('/posts/:id', upsertPost);
app.trace('/posts/:id', tracePost);
app.delete('/posts/:id', deletePost);
app.options('/posts/:id', getPostCors);
// special case for specifying both head and get
app.headGet('/files/*', serveFiles(`${import.meta.dir}/files`));
// any list of multiple verbs (must be uppercase)
app.on(['POST', 'PATCH'], '/posts/:id', addEditPost);
app.listen({ port: 3100 });
Included middleware
serveFiles
Serve static files from a directory. As shown above, serving static files is
easy with the serveFiles
middleware. Note that ranged requests are
supported, so you can use it for video streaming or partial downloads.
import { HttpRouter, serveFiles } from 'bunshine';
const app = new HttpRouter();
app.get('/public/*', serveFiles(`${import.meta.dir}/public`));
app.listen({ port: 3100 });
How to respond to both GET and HEAD requests:
import { HttpRouter, serveFiles } from 'bunshine';
const app = new HttpRouter();
app.on(['HEAD', 'GET'], '/public/*', serveFiles(`${import.meta.dir}/public`));
// or
app.headGet('/public/*', serveFiles(`${import.meta.dir}/public`));
app.listen({ port: 3100 });
How to alter the response provided by another handler:
import { HttpRouter, serveFiles } from 'bunshine';
const app = new HttpRouter();
const addFooHeader = async (_, next) => {
const response = await next();
response.headers.set('x-foo', 'bar');
return response;
};
app.get('/public/*', addFooHeader, serveFiles(`${import.meta.dir}/public`));
app.listen({ port: 3100 });
serveFiles accepts an optional second parameter for options:
import { HttpRouter, serveFiles } from 'bunshine';
const app = new HttpRouter();
app.get(
'/public/*',
serveFiles(`${import.meta.dir}/public`, {
extensions: ['html', 'css', 'js', 'png', 'jpg', 'gif', 'svg', 'ico'],
index: true,
})
);
app.listen({ port: 3100 });
All options for serveFiles:
| Option | Default | Description |
| ------------ | ----------- | ----------------------------------------------------------------------------------------- |
| acceptRanges | true
| If true, accept ranged byte requests |
| dotfiles | "ignore"
| How to handle dotfiles; allow=>serve normally, deny=>return 403, ignore=>run next handler |
| etag | N/A | Not yet implemented |
| extensions | []
| If given, a list of file extensions to allow |
| fallthrough | true
| If false, issue a 404 when a file is not found, otherwise proceed to next handler |
| maxAge | undefined
| If given, add a Cache-Control header with max-age† |
| immutable | false
| If true, add immutable directive to Cache-Control header; must also specify maxAge |
| index | []
| If given, a list of filenames (e.g. index.html) to look for when path is a folder |
| lastModified | true
| If true, set the Last-Modified header |
† A number in milliseconds or expression such as '30min', '14 days', '1y'.
cors
To add CORS headers to some/all responses, use the cors
middleware.
import { HttpRouter, cors } from 'bunshine';
const app = new HttpRouter();
// cors origin examples
app.use(cors({ origin: '*' }));
app.use(cors({ origin: true }));
app.use(cors({ origin: 'https://example.com' }));
app.use(cors({ origin: /^https:\/\// }));
app.use(cors({ origin: ['https://example.com', 'https://stuff.com'] }));
app.use(cors({ origin: ['https://example.com', /https:\/\/stuff.[a-z]+/i] }));
app.use(cors({ origin: incomingOrigin => incomingOrigin }));
app.use(cors({ origin: incomingOrigin => getAllowedOrigins(incomingOrigin) }));
// All options
app.use(
cors({
origin: 'https://example.com',
allowMethods: ['GET', 'POST'],
allowHeaders: ['X-HTTP-Method-Override', 'Authorization'],
exposeHeaders: ['X-Response-Id'],
maxAge: 86400,
credentials: true,
})
);
// and of course, cors can be attached at a specific path
app.all('/api', cors({ origin: '*' }));
// then add your endpoints
app.get('/api/hello', c => c.json({ hello: 'world' }));
app.listen({ port: 3100 });
Options details:
origin: A string, regex, array of strings/regexes, or a function that returns the desired origin header allowMethods: an array of HTTP verbs to allow clients to make allowHeaders: an array of HTTP headers to allow clients to send exposeHeaders: an array of HTTP headers to expose to clients maxAge: the number of seconds clients should cache the CORS headers credentials: whether to allow credentials (e.g. cookies or auth headers)
devLogger & prodLogger
devLogger
outputs colorful logs in the form below.
[timestamp] METHOD PATHNAME STATUS_CODE (RESPONSE_TIME)
example:
[19:10:50.276Z] GET /api/users/me 200 (5ms)
prodLogger
outputs logs in JSON with the following shape:
Request log:
{
"msg": "--> GET /",
"type": "request",
"date": "2021-08-01T19:10:50.276Z",
"id": "ea98fe2e-45e0-47d1-9344-2e3af680d6a7",
"host": "example.com",
"method": "GET",
"pathname": "/",
"runtime": "Bun v1.1.4",
"poweredBy": "Bunshine v1.0.0",
"machine": "server1",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"pid": 123
}
Response log:
{
"msg": "200 GET /",
"type": "response",
"date": "2021-08-01T19:10:50.286Z",
"id": "ea98fe2e-45e0-47d1-9344-2e3af680d6a7",
"host": "example.com",
"method": "GET",
"pathname": "/",
"runtime": "Bun v1.1.4",
"poweredBy": "Bunshine v1.0.0",
"machine": "server1",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"pid": 123,
"took": 5
}
To use these loggers, simply attach them as middleware.
import { HttpRouter, devLogger, prodLogger } from 'bunshine';
const app = new HttpRouter();
const logger = process.env.NODE_ENV === 'development' ? devLogger : prodLogger;
app.use(logger());
// or at a specific path
app.use('/api/*', logger());
app.listen({ port: 3100 });
performanceHeader
You can add an X-Took header with the number of milliseconds it took to respond.
import { HttpRouter, performanceHeader } from 'bunshine';
const app = new HttpRouter();
// Add X-Took header
app.use(performanceHeader());
// Or use a custom header name
app.use(performanceHeader('X-Time-Milliseconds'));
app.listen({ port: 3100 });
securityHeaders
You can add security-related headers to responses with the securityHeaders
middleware. For more information about security headers, checkout these
resources:
import { HttpRouter, securityHeaders } from 'bunshine';
const app = new HttpRouter();
app.use(securityHeaders());
// The following are defaults that you can override
app.use(
securityHeaders({
contentSecurityPolicy: {
frameSrc: ["'self'"],
workerSrc: ["'self'"],
connectSrc: ["'self'"],
defaultSrc: ["'self'"],
fontSrc: ['*'],
imgSrc: ['*'],
manifestSrc: ["'self'"],
mediaSrc: ["'self' data:"],
objectSrc: ["'self' data:"],
prefetchSrc: ["'self'"],
scriptSrc: ["'self'"],
scriptSrcElem: ["'self' 'unsafe-inline'"],
scriptSrcAttr: ["'none'"],
styleSrcAttr: ["'self' 'unsafe-inline'"],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'self'"],
sandbox: {},
},
crossOriginEmbedderPolicy: 'unsafe-none',
crossOriginOpenerPolicy: 'same-origin',
crossOriginResourcePolicy: 'same-origin',
permissionsPolicy: {
// only include special APIs that you use
accelerometer: [],
ambientLightSensor: [],
autoplay: ['self'],
battery: [],
camera: [],
displayCapture: [],
documentDomain: [],
encryptedMedia: [],
executionWhileNotRendered: [],
executionWhileOutOfViewport: [],
fullscreen: [],
gamepad: [],
geolocation: [],
gyroscope: [],
hid: [],
identityCredentialsGet: [],
idleDetection: [],
localFonts: [],
magnetometer: [],
midi: [],
otpCredentials: [],
payment: [],
pictureInPicture: [],
publickeyCredentialsCreate: [],
publickeyCredentialsGet: [],
screenWakeLock: [],
serial: [],
speakerSelection: [],
storageAccess: [],
usb: [],
webShare: ['self'],
windowManagement: [],
xrSpacialTracking: [],
},
referrerPolicy: 'strict-origin',
server: false,
strictTransportSecurity: 'max-age=86400; includeSubDomains; preload',
xContentTypeOptions: 'nosniff',
xFrameOptions: 'SAMEORIGIN',
xPoweredBy: false,
xXssProtection: '1; mode=block',
})
);
app.listen({ port: 3100 });
TypeScript pro-tips
Bun embraces TypeScript and so does Bunshine. Here are some tips for getting the most out of TypeScript.
Typing URL params
You can type URL params by passing a type to any of the route methods:
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
app.post<{ id: string }>('/users/:id', async c => {
// TypeScript now knows that c.params.id is a string
});
app.get<{ 0: string }>('/auth/*', async c => {
// TypeScript now knows that c.params['0'] is a string
});
app.listen({ port: 3100 });
Typing WebSocket data
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
// regular routes
app.get('/', c => c.text('Hello World!'));
type User = {
nickname: string;
email: string;
first: string;
last: string;
};
// WebSocket routes
app.socket.at<{ room: string }, { user: User }>('/games/rooms/:room', {
upgrade: ({ request, params, url }) => {
// Typescript knows that ws.data.params.room is a string
const cookies = req.headers.get('cookie');
const user = getUserFromCookies(cookies);
// here user is typed as User
return { user };
},
open(ws) {
// TypeScript knows that ws.data.params.room is a string
// TypeScript knows that ws.data.user is a User
},
message(ws, message) {
// TypeScript knows that ws.data.params.room is a string
// TypeScript knows that ws.data.user is a User
},
close(ws, code, message) {
// TypeScript knows that ws.data.params.room is a string
// TypeScript knows that ws.data.user is a User
},
});
// start the server
app.listen({ port: 3100 });
Roadmap
- ✅ HttpRouter
- ✅ SocketRouter
- ✅ Context
- ✅ examples/server.ts
- ✅ middleware > serveFiles
- ✅ middleware > cors
- ✅ middleware > devLogger
- ✅ middleware > prodLogger
- ✅ middleware > performanceHeader
- ✅ middleware > securityHeaders
- ✅ middleware > trailingSlashes
- 🔲 middleware > html rewriter
- 🔲 middleware > hmr
- 🔲 middleware > directoryListing
- 🔲 middleware > rate limiter
- 🔲 document headers middleware
- 🔲 move some middleware to
@bunshine/\*
? - ✅ gzip compression
- ✅ options for serveFiles
- 🔲 tests for cors
- 🔲 tests for devLogger
- 🔲 tests for prodLogger
- 🔲 tests for gzip
- 🔲 tests for responseFactories
- ✅ tests for serveFiles
- 🔲 100% test coverage
- 🔲 support and document flags to bin/serve.ts with commander
- 🔲 more files in examples folder
- 🔲 example of mini app that uses bin/serve.ts (maybe our own docs?)
- 🔲 GitHub Actions to run tests and coverage
- 🔲 Support server clusters
- ✅ Replace "ms" with a small and simple implementation
- ✅ Export functions to gzip strings and files
- ✅ Gzip performance testing (to get min/max defaults)