A Bun HTTP & WebSocket server that is a little ray of sunshine.
bun add bunshine
Or to run Bunshine on Node, install Nodeshine.
- 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
- Basic example
- Full example
- Serving static files
- Writing middleware
- Throwing responses
- WebSockets
- WebSocket pub-sub
- Server Sent Events
- Route Matching
- Included middleware
- TypeScript pro-tips
- Roadmap
- License
import { HttpRouter } from 'bunshine';
const app = new HttpRouter();
app.get('/', c => {
return new Response('Hello at ' + c.url.pathname);
});
app.listen({ port: 3100 });
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');
}
}
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 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.
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);
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]]]);
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 });
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' }));
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 (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);
Bunshine v1 used the path-to-regexp
package for processing path routes.
Due to a discovered
RegExp Denial of Service vulnerability,
Bunshine no longer uses
path-to-regexp docs.
Bunshine supports the following route matching features:
- Named placeholders using colons (e.g.
/posts/:id
) - End wildcards using stars (e.g.
/assets/*
) - Middle non-slash wildcards using stars (e.g.
/assets/*/*.css
) - Static paths (e.g.
/posts
) - Custom Regular Expression (e.g.
/^\/author\/([a-z]+)$/i
)
Support for other behaviors can lead to a Regular Expression Denial of service vulnerability where an attacker can request long URLs and tie up your server CPU with backtracking regular expression searches.
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/* |
/star/man/can |
{ 0: 'man/can' } |
/star/*/can |
/star/man/can |
{ 0: 'man' } |
/star/*/can/* |
/star/man/can/go |
{ 0: 'man', 1: 'go' } |
Note that all regular-expression special characters including
\ ^ $ * + ? . ( ) | { } [ ]
will be escaped. If you need any of these
behaviors, you'll need to pass in a RegExp
.
For example, the dot in /assets/*.js
will not match all characters--only dots.™™
Support for regex-like syntax has been dropped in v2 due to a
RegExp Denial of Service vulnerability.
For cases where you need to limit by character or specify optional segments,
you'll need to pass in a RegExp
. Be sure to check your RegExp
with a ReDoS
checker such as Devina or
redos-checker on npm.
Example | Explaination | Equivalent RegExp |
---|---|---|
/users/([a-z-]+)/ |
Character classes are not supported | ^\/users\/([a-z-]+)$ |
/users/(\\d+) |
Character class escapes are not supported | ^/\/users\/(\d+)$ |
/(users|u)/:id |
Pipes are not supported | ^\/(users|u)/([^/]+)$ |
/:a/:b? |
Optional params are not supported | ^\/([^/]*)\/(.*)$ |
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);
// regular expression matchers are supported
app.get(/^\/author\/([a-z]+)$/i, getPost);
app.listen({ port: 3100 });
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'.
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
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 v2.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 v2.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 });
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 });
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 });
Bun embraces TypeScript and so does Bunshine. Here are some tips for getting the most out of TypeScript.
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 });
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 });
- ✅ 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)