Phillip England
An Express-like HTTP Library for Bun
Read the docs
1bun add github:phillip-england/xerus
1import { HTTPContext, logger, Xerus } from "xerus/xerus";
2
3let app = new Xerus()
4
5app.use(logger)
6
7app.get("/static/*", async (c: HTTPContext) => {
8 return await c.file("." + c.path);
9});
10
11app.get('/', async (c: HTTPContext) => {
12 return c.html(`<h1>O'Doyle Rules!</h1>`)
13})
14
15await app.listen()
An HTTPHandlerFunc takes in an HTTPContext and returns Promise<Response>:
1let handler = async (c: HTTPContext) => {
2 return c.html(`<h1>O'Doyle Rules</h1>`)
3}
4
5app.get('/', handler)
Xerus supports static, dynamic, and wildcard paths:
1app.get('/', handler)
2app.get('/user/:id', handler)
3app.get('/static/*', handler)
Group routing is also supported:
1app.group('/api')
2 .post('/user/:id', handler)
3 .post('/user/post/:postNumber', handler)
Xerus offers a file-based routing system which can be used as follows:
1import { FileRouter } from "xerus";
2
3let router = await FileRouter.new({
4 "src": path.join(process.cwd(), "app"),
5 "port": 8080,
6});
Assuming ./app is your project root, ./app/+init.tsx is where you can run any init logic:
Below, I set up logging and serving static files.
./app/+init.tsx:
1let module = new InitModule();
2
3module.init(async (app: Xerus) => {
4 app.use(logger);
5 app.static("static");
6});
7
8export default module;
9
Assuming ./app is your project root, ./app/+route.tsx will provide the logic for all routes hitting /:
./app/+route.tsx:
1let module = new RouteModule();
2
3module.get(async (c: HTTPContext) => {
4 return c.jsx(
5 <GuestLayout title="Home Page">
6 <h1>Home Page!</h1>
7 </GuestLayout>,
8 );
9});
10
11export default module;
For /about, place the following in ./app/about/route.tsx:
1let module = new RouteModule();
2
3module.get(async (c: HTTPContext) => {
4 return c.jsx(
5 <GuestLayout title="About Page">
6 <h1>Home Page!</h1>
7 </GuestLayout>,
8 );
9});
10
11export default module;
For dynamic routing, try ./app/user/:id/+route.tsx:
1let module = new RouteModule();
2
3module.get(async (c: HTTPContext) => {
4 return c.jsx(
5 <GuestLayout title="User Page">
6 <h1>User {c.getParam('id')}</h1>
7 </GuestLayout>,
8 );
9});
10
11export default module;
Middlware is export out of +route.tsx file. All middlware can be of type Cascade or Isolate.
In short, Cascade middleware will pour down onto any file beneath itself in the filesystem whereas Isolate middleware will only affect the route it is exported from.
For example, here we apply the same middleware, once as Isolate, then again as Cascade:
1const testmw = new Middleware(
2 async (c: HTTPContext, next: MiddlewareNextFn) => {
3 console.log("before");
4 await next();
5 console.log("after");
6 },
7);
8
9
10let module = new RouteModule();
11
12module.get(async (c: HTTPContext) => {
13 return c.jsx(
14 <GuestLayout title="Home Page">
15 <h1>User {c.getParam('id')}</h1>
16 </GuestLayout>,
17 );
18}, isolate(testmw), cascade(testmw));
19
20export default module;
Middleware is excuted from top to bottom in sync with the filesystem.
Use a wildcard to serve static files from ./static:
1app.get("/static/*", async (c: HTTPContext) => {
2 return await c.file("." + c.path);
3});
Middleware executes in the following order:
Create a new Middleware:
1let mw = new Middleware(
2 async (c: HTTPContext, next: MiddlewareNextFn): Promise<void | Response> => {
3 console.log('logic before handler');
4 next();
5 console.log("logic after handler");
6 },
7);
Link it globally:
1app.use(mw)
Or to a group:
1app.group('/api', mw) // <=====
2 .post('/user/:id', handler)
3 .post('/user/post/:postNumber', handler)
Or to a route:
1app.get('/', handler, mw) // <=====
Chain as many as you'd like to all three types:
1app.use(mw, mw, mw)
2
3app.group('/api', mw, mw, mw)
4 .post('/user/:id', handler)
5 .post('/user/post/:postNumber', handler)
6
7app.get('/', handler, mw, mw, mw)
HTTPContext allows us to work with the incoming requests and prepare responses. Here are the features it provides.
1app.get('/', async (c: HTTPContext) => {
2 return c.html(`<h1>O'Doyle Rules</h1>`)
3})
4
5app.get('/redirect', async(c: HTTPContext) => {
6 return c.redirect('/')
7})
Use the BodyType enum to enforce a specific type of data in the request body:
1app.post('/body/text', async (c: HTTPContext) => {
2 let data = await c.parseBody(BodyType.TEXT)
3 return c.json({data: data})
4})
5
6app.post('/body/json', async (c: HTTPContext) => {
7 let data = await c.parseBody(BodyType.JSON)
8 return c.json({data: data})
9})
10
11app.post('/body/multipart', async (c: HTTPContext) => {
12 let data = await c.parseBody(BodyType.MULTIPART_FORM)
13 return c.json({data: data})
14})
15
16app.post('/body/form', async (c: HTTPContext) => {
17 let data = await c.parseBody(BodyType.FORM)
18 return c.json({data: data})
19})
1app.get('/user/:id', async (c: HTTPContext) => {
2 let id = c.getParam('id')
3 return c.html(`<h1>O'Doyle Rules Times ${id}!</h1>`)
4})
1app.get('/', async (c: HTTPContext) => {
2 return c.setStatus(404).html(`<h1>O'Doyle Not Found</h1>`)
3})
1app.get('/', async (c: HTTPContext) => {
2 c.setHeader('X-Who-Rules', `O'Doyle Rules`)
3 return c.html(`<h1>O'Doyle Rules!</h1>`)
4})
1app.get('/', async (c: HTTPContext) => {
2 let headerVal = c.getHeader('X-Who-Rules')
3 if (headerVal) {
4 return c.html(`<h1>${headerVal}</h1>`)
5 }
6 return c.html(`<h1>Header missing</h1>`)
7})
1app.get('/html', async (c: HTTPContext) => {
2 return c.html(`<h1>O'Doyle Rules!</h1>`)
3})
4
5app.get('/json', async (c: HTTPContext) => {
6 return c.json({message: `O'Doyle Rules!`})
7})
8
9app.get('/text', async (c: HTTPContext) => {
10 return c.text(`O'Doyle Rules!`)
11})
1app.get('/', async (c: HTTPContext) => {
2 const stream = new ReadableStream({
3 start(controller) {
4 const encoder = new TextEncoder();
5 let count = 0;
6 const interval = setInterval(() => {
7 controller.enqueue(encoder.encode(`O'Doyle Rules! ${count}\n`));
8 count++;
9 if (count >= 3) {
10 clearInterval(interval);
11 controller.close();
12 }
13 }, 1000);
14 }
15 });
16 c.setHeader("Content-Type", "text/plain");
17 c.setHeader("Content-Disposition", 'attachment; filename="odoyle_rules.txt"');
18 return c.stream(stream);
19});
1app.get('/', async (c: HTTPContext) => {
2 return c.file("./path/to/file");
3});
1app.get('/', async (c: HTTPContext) => {
2 return c.file("./path/to/file", true);
3});
1app.get('/set', async (c: HTTPContext) => {
2 c.setCookie('secret', "O'Doyle_Rules!")
3 return c.redirect('/get')
4});
5
6app.get('/get', async (c: HTTPContext) => {
7 let cookie = c.getCookie('secret')
8 if (cookie) {
9 return c.text(`visit /clear to clear the cookie with the value: ${cookie}`)
10 }
11 return c.text('visit /set to set the cookie')
12})
13
14app.get('/clear', async (c: HTTPContext) => {
15 c.clearCookie('secret')
16 return c.redirect('/get')
17})
1app.onNotFound(async (c: HTTPContext): Promise<Response> => {
2 return c.setStatus(404).text("404 Not Found");
3});
1app.onErr(async (c: HTTPContext): Promise<Response> => {
2 let err = c.getErr();
3 console.error(err);
4 return c.setStatus(500).text("internal server error");
5});
Setup a new websocket route, using onConnect for pre-connect authorization:
1app.ws("/chat", {
2 async open(ws) {
3 let c = ws.data // get the context
4
5 },
6 async message(ws, message) {
7
8 },
9 async close(ws, code, message) {
10
11 },
12 async onConnect(c: WSContext) {
13 c.set('secret', "O'Doyle") // set pre-connect data
14 }
15});