BunSai
Bonsai is a japanese art of growing and shaping miniature trees in containers
BIG NOTE
As the version implies (v0.x.x), this API is not yet stable and can be breaking changed without warnings.
Quick start
BunSai is a full-stack, zero dependency, agnostic framework for the web, built upon Bun (in fact, it has Nunjucks, Sass and Stylus as optional dependencies). You can install it:
bun add bunsai
And use it as a handler:
import BunSai from "bunsai";
const { fetch } = new BunSai({
});
Bun.serve({
fetch,
});
How it works?
Powered by Bun.FileSystemRouter
and some fancy tricks, BunSai takes an approach where you declare the files you want to become "routes"
new BunSai({
loaders: {
".ext": loaderInitiator,
},
});
And all files with that file extension will be served as routes.
Example
Lets say you have the following files:
pages
├── index.njk
├── settings.tsx
├── blog
│ ├── [slug].svelte
│ └── index.ts
└── [[...catchall]].vue
You can configure BunSai to serve those files:
new BunSai({
loaders: {
".njk": nunjucksLoaderInit,
".ts": apiLoaderInit,
".tsx": reactLoaderInit,
".svelte": svelteLoaderInit,
".vue": vueLoaderInit,
},
});
Check the LoaderInitiator
interface
You can also specify file extensions that will be served staticly (return new Response(Bun.file(filePath))
), like so:
new BunSai({
staticFiles: [".jpg", ".css", ".aac"],
});
There is a caveat around staticFiles
: as all files are served using the FileSystemRouter, pages/pic.jpeg
will be served as /pic
Built-in loaders
BunSai is 100% flexible, but this does not mean that it cannot be opinionated. BunSai ships with built-in (optional) loaders:
Since v0.1.0. Last change v0.3.0
Nunjucks is a rich powerful templating language with block inheritance, autoescaping, macros, asynchronous control, and more. Heavily inspired by jinja2.
bun add nunjucks @types/nunjucks
import getNunjucksLoader from "bunsai/loaders/nunjucks";
const nunjucks =
getNunjucksLoader();
new BunSai({
loaders: {
".njk": nunjucks.loaderInit,
},
});
nunjucks.env;
<body>
{# 'server', 'route' and 'request' #}
<p>
All those objects are passed to the Nunjucks renderer to be available
globally
</p>
</body>
Since v0.1.0. Last change v0.2.0
Sass is the most mature, stable, and powerful professional grade CSS extension language in the world.
bun add sass
import getSassLoader from "bunsai/loaders/sass";
const loaderInit = getSassLoader();
new BunSai({
loaders: {
".scss": loaderInit,
},
});
Since v0.3.0
Stylus is an expressive, robust, feature-rich CSS language. It's compiler is a bit less performant than Sass, but I thought it was a nice feature to add.
bun add stylus
import getStylusLoader from "bunsai/loaders/stylus";
const loaderInit = getStylusLoader();
new BunSai({
loaders: {
".styl": loaderInit,
},
});
Module
Since v0.1.0. Last change v0.3.0
BunSai offers a simple module implementation to handle .ts
, .tsx
, .js
and .node
files:
import { ModuleLoaderInit } from "bunsai/loaders";
new BunSai({
loaders: {
".ts": ModuleLoaderInit,
},
});
A server module is a regular TS/TSX/JS/NAPI (anything that Bun can import) file that have the following structure:
export const headers = {
};
export function invalidate(data: ModuleData) {
}
export function handler(data: ModuleData) {
}
Recommended
Since v0.1.0. Last change v0.3.0
If you liked BunSai's opinion and want to enjoy all this beauty, you can use the recommended configuration:
import getRecommended from "bunsai/recommended";
const { loaders, staticFiles, middlewares, nunjucks } =
getRecommended();
new BunSai({
loaders,
staticFiles,
middlewares,
});
nunjucks.env();
Check the Recommended
interface.
Middlewares
Middlewares can be used both on "start up" and during lifetime.
Builtin Middlewares
Since v0.3.0
Builtin middlewares are Middleware class extensions and can be used on BunSai construction:
new BunSai({
middlewares: [new Middleware()],
});
And during lifetime (using the inject
static method):
const { middlewares } = new BunSai();
Middleware.inject(middlewares );
At this moment, BunSai ships with:
- DDOS
import DDOS from "bunsai/middlewares/ddos"
- CORS
import CORS, { CORSPreflight, CORSResponse } from "bunsai/middlewares/cors"
To ask for more middlewares, click here
Creating you own distributable middleware
A distributable middleware should extend the Middleware abstract class
import Middleware from "bunsai/internals/middleware";
export default class MyMiddleware extends Middleware<
"response" | "request" | "notFound" | "error"
> {
name = "unique name";
runsOn = "response" | "request" | "notFound" | "error";
protected $runner: MiddlewareRunnerWithThis<MiddlewareData, this> = function (
data
) {
};
}
BunSai Middleware Record
During lifetime, BunSai categorizes middlewares into 4 groups:
Response Middlewares
Since v0.1.0. Last change v0.2.0
You can use response middlewares to override or customize the response given by the loader.
const { middlewares } = new BunSai();
middlewares.response
.add("name", (data) => {
return new Response();
return data.response;
})
.add();
middlewares.response.remove("name").remove();
Request Middlewares
Since v0.1.0. Last change v0.2.0
You can use request middlewares to do things before anything else, like sending an early response (e.g. 429 Too Many Requests).
const { middlewares } = new BunSai();
middlewares.request
.add("name", (data) => {
return new Response();
})
.add();
middlewares.request.remove("name").remove();
"Not Found" Middlewares
Since v0.1.0. Last change v0.2.0
"Not Found" middlewares are only called when the router did not found the asset. The main purpose of the NF middleware is to override the default behavior (sending an empty 404 response).
const { middlewares } = new BunSai();
middlewares.notFound
.add("name", (data) => {
})
.add();
middlewares.notFound.remove("name").remove();
Error Middlewares
Since v0.3.0
"Error" middlewares are only called when something went really wrong. The main purpose of the error middleware is to override the default behavior (let Bun handle it).
const { middlewares } = new BunSai();
middlewares.error
.add("name", (data) => {
})
.add();
middlewares.error.remove("name").remove();
Utils
Router
Since v0.3.0
Router was designed to be a facilitator in building APIs that use the Module loader.
It makes more sense to use Router on files that use the following filename syntaxes: [...catch-all] | [[...optional-catch-all]] | [dynamic]
The Router is a simple utility that abstracts the workflow of an HTTP API.
HTTP methods are classified as class methods.
Rules
- If you did not declare any 'POST' handler and the client made a POST request, the Router will automatically return
405 Method Not Allowed
(e.g.); - If none of the matchers returned
true
for the given path, the Router returns 404 Not Found
; - If the handler call chain has ended, but no response was given,
501 Not Implemented
is returned and with the following status text: '%pathname%' handlers returned nothing
;
Matchers
- String: Router will use the
String.endsWith
approach, except if the string is '*'
which has the default wildcard behavior; - RegExp:
regex.test(route.pathname)
will be used; - Function: return
true
if the request should be accepted; - Array: an array containing any of the previous matchers.
Array.some
approach will be used;
Implementing
import { Router } from "bunsai/util";
import Router from "bunsai/util/router";
function matcher({ pathname }) {
return pathname == "/c";
}
export const { handler } = new Router()
.get("/a", ({ response }) =>
response(new Response(null, { status: 204 }))
)
.post(
/\/b/,
() => {
return new Response(null, { status: 206 });
},
() => {
}
)
.put(matcher, () => {})
.delete();
Tips
- Since Router always return a Response object, the
headers
object will be ignored by the loader. - The fastest matcher is
"*"
. - The second fastest matcher is RegExp.
- Never declare strings, RegExp, arrays or functions that have equal structures (or function names);
when the Router declares your handler on the channel it uses
matcher.name ?? matcher.toString()
. - Also avoid unnamed function matchers, as an unnamed function 'name' property will always be an empty string;
this way you will avoid
Error: '' already exists on this middleware channel
when declaring two unnamed functions on the GET channel (e.g.).