Xerus
A Express-like HTTP Library for Bun
Installation
bun add github:phillip-england/xerus
Hello, World
Create an index.ts
and paste in the following code:
import { HTTPContext, logger, Xerus } from "xerus/xerus";
let app = new Xerus()
app.use(logger)
app.get("/static/*", async (c: HTTPContext) => {
return await c.file("." + c.path);
});
app.get('/', async (c: HTTPContext) => {
return c.html(`<h1>O'Doyle Rules!</h1>`)
})
await app.listen()
Run the application using:
bun run --hot index.ts
Visit localhost:8080
HTTPHandlerFunc
An HTTPHandlerFunc
takes in an HTTPContext
and returns Promise<Response>
:
let handler = async (c: HTTPContext) => {
return c.html(`<h1>O'Doyle Rules</h1>`)
}
app.get('/', handler)
Routing
Xerus
supports static, dynamic, and wildcard paths:
app.get('/', handler)
app.get('/user/:id', handler)
app.get('/static/*', handler)
Group routing is also supported:
app.group('/api')
.post('/user/:id', handler)
.post('/user/post/:postNumber', handler)
File Based Routing
I have another package, squid
, which abstracts over xerus
and extends it for file-based routing. Checkout the README here if you are interested.
Here is the quickstart for squid
:
import { Squid } from "squid"
let result = await Squid.new("./app", process.cwd())
if (result.isErr()) {
console.error(result.unwrapErr())
}
let app = result.unwrap() as Squid
await app.listen()
Static Files
Use a wildcard to serve static files from ./static
:
app.get("/static/*", async (c: HTTPContext) => {
return await c.file("." + c.path);
});
Middleware
Middleware executes in the following order:
- Global
- Group
- Route
Create a new Middleware
:
let mw = new Middleware(
async (c: HTTPContext, next: MiddlewareNextFn): Promise<void | Response> => {
console.log('logic before handler');
next();
console.log("logic after handler");
},
);
Link it globally:
app.use(mw)
Or to a group:
app.group('/api', mw) // <=====
.post('/user/:id', handler)
.post('/user/post/:postNumber', handler)
Or to a route:
app.get('/', handler, mw) // <=====
Chain as many as you'd like to all three types:
app.use(mw, mw, mw)
app.group('/api', mw, mw, mw)
.post('/user/:id', handler)
.post('/user/post/:postNumber', handler)
app.get('/', handler, mw, mw, mw)
HTTPContext
HTTPContext
allows us to work with the incoming requests and prepare responses. Here are the features it provides.
Redirect The Request
app.get('/', async (c: HTTPContext) => {
return c.html(`<h1>O'Doyle Rules</h1>`)
})
app.get('/redirect', async(c: HTTPContext) => {
return c.redirect('/')
})
Parse The Request Body
Use the BodyType
enum to enforce a specific type of data in the request body:
app.post('/body/text', async (c: HTTPContext) => {
let data = await c.parseBody(BodyType.TEXT)
return c.json({data: data})
})
app.post('/body/json', async (c: HTTPContext) => {
let data = await c.parseBody(BodyType.JSON)
return c.json({data: data})
})
app.post('/body/multipart', async (c: HTTPContext) => {
let data = await c.parseBody(BodyType.MULTIPART_FORM)
return c.json({data: data})
})
app.post('/body/form', async (c: HTTPContext) => {
let data = await c.parseBody(BodyType.FORM)
return c.json({data: data})
})
Get Dynamic Path Param
app.get('/user/:id', async (c: HTTPContext) => {
let id = c.getParam('id')
return c.html(`<h1>O'Doyle Rules Times ${id}!</h1>`)
})
Set Status Code
app.get('/', async (c: HTTPContext) => {
return c.setStatus(404).html(`<h1>O'Doyle Not Found</h1>`)
})
Set Response Headers
app.get('/', async (c: HTTPContext) => {
c.setHeader('X-Who-Rules', `O'Doyle Rules`)
return c.html(`<h1>O'Doyle Rules!</h1>`)
})
Get Request Header
app.get('/', async (c: HTTPContext) => {
let headerVal = c.getHeader('X-Who-Rules')
if (headerVal) {
return c.html(`<h1>${headerVal}</h1>`)
}
return c.html(`<h1>Header missing</h1>`)
})
Respond with HTML, JSON, or TEXT
app.get('/html', async (c: HTTPContext) => {
return c.html(`<h1>O'Doyle Rules!</h1>`)
})
app.get('/json', async (c: HTTPContext) => {
return c.json({message: `O'Doyle Rules!`})
})
app.get('/text', async (c: HTTPContext) => {
return c.text(`O'Doyle Rules!`)
})
Stream A Response
app.get('/', async (c: HTTPContext) => {
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
let count = 0;
const interval = setInterval(() => {
controller.enqueue(encoder.encode(`O'Doyle Rules! ${count}\n`));
count++;
if (count >= 3) {
clearInterval(interval);
controller.close();
}
}, 1000);
}
});
c.setHeader("Content-Type", "text/plain");
c.setHeader("Content-Disposition", 'attachment; filename="odoyle_rules.txt"');
return c.stream(stream);
});
Response With A File
app.get('/', async (c: HTTPContext) => {
return c.file("./path/to/file");
});
Stream A File
app.get('/', async (c: HTTPContext) => {
return c.file("./path/to/file", true);
});
Set, Get, And Clear Cookies
app.get('/set', async (c: HTTPContext) => {
c.setCookie('secret', "O'Doyle_Rules!")
return c.redirect('/get')
});
app.get('/get', async (c: HTTPContext) => {
let cookie = c.getCookie('secret')
if (cookie) {
return c.text(`visit /clear to clear the cookie with the value: ${cookie}`)
}
return c.text('visit /set to set the cookie')
})
app.get('/clear', async (c: HTTPContext) => {
c.clearCookie('secret')
return c.redirect('/get')
})
Custom 404
app.onNotFound(async (c: HTTPContext): Promise<Response> => {
return c.setStatus(404).text("404 Not Found");
});
Custom Error Handling
app.onErr(async (c: HTTPContext): Promise<Response> => {
let err = c.getErr();
console.error(err);
return c.setStatus(500).text("internal server error");
});
Web Sockets
Setup a new websocket route, using onConnect
for pre-connect authorization:
app.ws("/chat", {
async open(ws) {
let c = ws.data // get the context
},
async message(ws, message) {
},
async close(ws, code, message) {
},
async onConnect(c: WSContext) {
c.set('secret', "O'Doyle") // set pre-connect data
}
});