02: Bun API
Bun utilities
Sleep
Bun.sleep(ms): async method that sleeps for specified number of millisecondsBun.sleepSync(ms): sync method that sleeps for specified number of milliseconds
file urls
To convert file paths to a from a file url protocol (file://), you can use these two methods:
Bun.fileURLToPath(url: URL): takes in aURLobject representing a file url and converts it into a filesystem pathBun.pathToFileURL(path: string): takes in a filepath and converts it to its file url representation by returning aURLinstance.
const path = Bun.fileURLToPath(new URL("file:///foo/bar.txt"));
console.log(path); // "/foo/bar.txt"
const url = Bun.pathToFileURL("/foo/bar.txt");
console.log(url); // "file:///foo/bar.txt"
Bun color
The Bun.color() method offers a robust API for converting color strings into different formats, like hex, CSS, RGB, and ansi.
Bun.color("#ff0000", "css"); // => "red"
Bun.color("rgb(255, 0, 0)", "css"); // => "red"
Bun.color("red", "ansi"); // => "\x1b[31m"
Bun.color("#f00", "ansi-16m"); // => "\x1b[38;2;255;0;0m"
Bun.color(0xff0000, "ansi-256"); // => "\u001b[38;5;196m"
Bun.color({ r: 255, g: 0, b: 0 }, "number"); // => 16711680
Bun.color("hsl(0, 0%, 50%)", "{rgba}"); // => { r: 128, g: 128, b: 128, a: 1 }
Printing out tables
The Bun.inspect.table() method takes in a type of T[] to console log the data in a tabular format:
console.log(
Bun.inspect.table([
{ a: 1, b: 2, c: 3 },
{ a: 4, b: 5, c: 6 },
{ a: 7, b: 8, c: 9 },
]),
);
// ┌───┬───┬───┬───┐
// │ │ a │ b │ c │
// ├───┼───┼───┼───┤
// │ 0 │ 1 │ 2 │ 3 │
// │ 1 │ 4 │ 5 │ 6 │
// │ 2 │ 7 │ 8 │ 9 │
// └───┴───┴───┴───┘
Base 64 encoding and decoding
The btoa(string) converts a string to a base64 representation and atob(base64string) converts a base64 string back to its original contents. These are web standard methods.
const data = "hello world";
const encoded = btoa(data); // => "aGVsbG8gd29ybGQ="
const decoded = atob(encoded); // => "hello world"
Compressing data with gzip
Using the Bun.gzipSync(unint8arr) and Bun.gunzipSync(unint8arr), you can can compress and decompress UInt8Array instances of binary data using GZIP.
const data = Buffer.from("Hello, world!");
const compressed = Bun.gzipSync(data);
// => Uint8Array
const decompressed = Bun.gunzipSync(compressed);
// => Uint8Array
HTMLRewriter
This bun api lets you parse HTML strings and do stuff with them using a DOM API, and then turn it back into a string. Really useful for generating server-side HTML to server render.
More info:
// Replace all images with a rickroll
const rewriter = new HTMLRewriter().on("img", {
element(img) {
// Famous rickroll video thumbnail
img.setAttribute(
"src",
"https://img.youtube.com/vi/dQw4w9WgXcQ/maxresdefault.jpg",
);
// Wrap the image in a link to the video
img.before(
'<a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ" target="_blank">',
{ html: true },
);
img.after("</a>", { html: true });
// Add some fun alt text
img.setAttribute("alt", "Definitely not a rickroll");
},
});
// An example HTML document
const html = `
<html>
<body>
<img src="/cat.jpg">
<img src="dog.png">
<img src="https://example.com/bird.webp">
</body>
</html>
`;
const result = rewriter.transform(html);
console.log(result);
Bun stream API
Bun offers a bunch of useful helpers abstracting over streams.
const stream = (await fetch("https://bun.sh")).body;
stream; // => ReadableStream
await Bun.readableStreamToArrayBuffer(stream);
// => ArrayBuffer
await Bun.readableStreamToBytes(stream);
// => Uint8Array
await Bun.readableStreamToBlob(stream);
// => Blob
await Bun.readableStreamToJSON(stream);
// => object
await Bun.readableStreamToText(stream);
// => string
// returns all chunks as an array
await Bun.readableStreamToArray(stream);
// => unknown[]
// returns all chunks as a FormData object (encoded as x-www-form-urlencoded)
await Bun.readableStreamToFormData(stream);
// returns all chunks as a FormData object (encoded as multipart/form-data)
await Bun.readableStreamToFormData(stream, multipartFormBoundary);
Bun CLI API
Spawning processes
Bun Shell
Starting from bun v1.0.24 and on, you can use the bun shell. The great thing about this is that you can treat variables of type Response, ArrayBuffer, and Blob just as you would strings.
import { $ } from "bun";
const response = await fetch("https://example.com");
// Use Response as stdin.
await $`cat < ${response} | wc -c`; // 1256
Here are some basic methods you can tack on to the end of the tempalte literal method:
const { stdout, stderr } = await $`echo "Hello World!"`.quiet(); // No output
const welcome = await $`echo "Hello World!"`.text(); // returns as text
quiet(): instead of printing standard out, returns an object with thestdout,stderr, andexitCodeas properties.nothrow(): doesn't throw on an error.text(): returns the standard out of the command as text.json(): reads the output of a command as JSONblob(): reads the output of a command as a blob
const { stdout, stderr, exitCode } = await $`something-that-may-fail`
.nothrow()
.quiet();
Piping and Redirecting with JavaScript Objects
- You can redirect content to a buffer, blob, or file.
- You can use these files as standard input: buffer, blob, file,
Response
import { $ } from "bun";
// redirect output into a buffer
const buffer = Buffer.alloc(100);
await $`echo "Hello World!" > ${buffer}`;
console.log(buffer.toString()); // Hello World!\n
import { $ } from "bun";
const response = new Response("hello i am a response body");
const result = await $`cat < ${response}`.text();
console.log(result); // hello i am a response body
Setting environment variables
You can use the bun shell to set all environment variables, useing the $.env(obj) command. Just make sure not to override everything.
import { $ } from "bun";
$.env({ ...process.env, FOO: "bar" });
Changing directories
You can change the working directory of a command by passing a string to .cwd():
import { $ } from "bun";
// temporarily change working directory to /tmp
await $`pwd`.cwd("/tmp"); // /tmp
Iterating through standard output
import { $ } from "bun";
for await (let line of $`echo "Hello World!"`.lines()) {
console.log(line); // Hello World!
}
Brace expansion
import { $ } from "bun";
await $.braces(`echo {1,2,3}`);
// => ["echo 1", "echo 2", "echo 3"]
Files
Writing files
Use the async Bun.write(filepath, data) method to write data to a file
await Bun.write("output.txt", "this is output file")
The data can be a string, Blob, BunFile, ArrayBuffer, or a Response.
File API
You can get a file representation in bun by using the async Bun.file(filepath) method, which returns a file.
const file = await Bun.file("output.txt")
file.exists(): async method that returns whether the file existsfile.size: returns the file size in number of bytesfile.type: returns the MIME type of the file.
This is how you change the type of a file you create.
const notreal = Bun.file("notreal.json", { type: "application/json" });
Here is how you write a file to Standard output:
const input = Bun.file("input.txt");
await Bun.write(Bun.stdout, input);
Reading Files
One a bun file object, you have these async methods to read data in from different specified encodings:
file.text(): returns the utf8 text file contentfile.stream(): returns the file content as a readable streamfile.arrayBuffer(): returns the file content as an array bufferfile.blob(): returns the file content as a Blob instance
Fetching data
DNS prefetching
WIth bun, you can perform DNS prefetching to make fetching websites and APIs even faster:
import { dns } from "bun";
// ...on startup
dns.prefetch("example.com");
// ...later on
await fetch("https://example.com/");
SQLite
Setup
SQLite in bun is synchronous and has a good API.
import { Database } from "bun:sqlite";
const db = new Database();
const query = db.query("select 'Hello world' as message;");
query.get(); // => { message: "Hello world" }
- Create a new database by instantiating the
Databaseclass. You can create either an in-memory database or a persisting database.
const db = new Database(); // in-memory
const db = new Database("mydb.sqlite", {create: true}); // file
- Create a query with the
db.query(querystring)method, which return a query object. You can then execute the query withquery.get()
const query = db.query("select 'Hello world' as message;");
query.get();
Queries
You can pass parameters to queries. and then execute them with methods. We define parameters with a $ prefixed on the variable name.
// $param1 and $param2 are now variables we use later
const query = db.query(`SELECT $param1, $param2;`);
// pass values for $param1 and $param2
const data = query.get({
$param1: "column1",
$param2: "column2"
})
query.get(): returns the first result as an objectquery.all(): returns all results as an array of objectsquery.run(): runs the query, returns nothing.query.toString(): expands the query as a string, plugging in parameters. Useful for debugging.
You can also remove the dollar signs and make sure you are passing in arguments correctly by enabling the strict: true option when creating a new database:
import { Database } from "bun:sqlite";
const db = new Database(":memory:", {
strict: true,
});
const query = db.query(`select $message;`);
query.all({
message: "Hello world"
});
Type safety
When you query a SQL database, you often want to map your query results to a JavaScript object.
You can now use query.as(Class) to map query results to instances of a class. This lets you attach methods, getters, and setters without using an ORM.
import { Database } from "bun:sqlite";
class Tweet {
id: number;
text: string;
username: string;
get isMe() {
return this.username === "jarredsumner";
}
}
const db = new Database("tweets.db");
// query and map object to class without TS bullshit
const tweets = db.query("SELECT * FROM tweets")
.as(Tweet);
for (const tweet of tweets.all()) {
if (!tweet.isMe) {
console.log(`${tweet.username}: ${tweet.text}`);
}
}
iteration
If you want to use generators to iterate over a large number of records, the bun sqlite lets you do that:
import { Database } from "bun:sqlite";
class User {
id: number;
email: string;
}
const db = new Database("users.db");
const rows = db.query("SELECT * FROM users")
.as(User)
.iterate();
for (const row of rows) {
console.log(row);
}
You can also directly iterate over the query object.
for (const row of db.query("SELECT * FROM users")) {
console.log(row); // { id: 1, email: "hello@bun.sh" }
}
Testing
Basic testing
This is how you create basic tests in bun.
- Tests should end in the file extension
.test.ts - Run tests with
bun test
import { expect, test } from "bun:test";
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
You create a test with test(name, func), and you create a test suite with describe(name, func).
import { expect, test, describe } from "bun:test";
describe("arithmetic", () => {
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
test("2 * 2", () => {
expect(2 * 2).toBe(4);
});
});
You can even specify a timeout in milliseconds for how long the test should take, else it will fail if it goes over the timeout.
test(name, cb, timeoutMillis)
test("my test", () => {}, 500)
Testing with flags
Sometimes you want to run only some tests and not others. You can use these test methods to do that, and then run only those matching tests with CLI options on the bun test command.
test.skip(name, cb): skips the testtest.todo(name, cb): marks the test as a TODO test, meaning the test runner expects them to fail since they're supposed to be prematurely done. To exclusively run TODO tests, runbun test --todotest.only(name, cb): marks tests, andbun test --onlyruns only those tests.
conditional testing
Conditional testing allows you to run or skip tests based on some condition
test.if(condition)(name, cb): runs the test if the condition is truetest.skipif(condition)(name, cb): skips the test if the condition is truetest.todoif(condition)(name, cb): marks the test as a todo test if the condition is true
test.if(Math.random() > 0.5)("runs half the time", () => {
// ...
});
const macOS = process.arch === "darwin";
test.if(macOS)("runs on macOS", () => {
// runs if macOS
});
Bun server side
Creating servers with Bun.serve()
The Bun.serve() command is a super fast way to spin up a performant server, built upon web standards. This is the basic syntax of the Bun.serve() method:
const server = Bun.serve(options)
The options object has these keys and properties:
port: the port to listen onfetch(req): this callback triggers on each request to the server. It takes in a web standardRequestinstance and must return a web standardResponseinstance.routes: an object that maps route strings to their route handlersdevelopment: a boolean that if set, enables hot reloading on the server.
The Bun.serve() method returns a server object with these properties and methods:
server.reload(staticRoutes): takes in a static routes configurationserver.port: the port the server is listening on
basic server
const server = Bun.serve({
port: 3000, // Optional; defaults to process.env.PORT || 3000
fetch(request: Request): Response | Promise<Response> {
return new Response("Welcome to Bun!");
},
});
console.log(`Listening on http://localhost:${server.port}`);
routing
By specifying the routes option of type Record<string, Handler>, bun offers an easy way to provide route handlers:
Bun.serve({
routes: {
"/api/version": async () => {
// Example: fetch version from database
return Response.json({ version: "1.0.0" });
},
"/orgs/:orgId/repos/:repoId": req => {
const { orgId, repoId } = req.params;
return Response.json({ orgId, repoId });
},
},
});
New v1.2 static HTML imports
The cool thing about bun v1.2 is that you can import HTML files just like that as text content into your file. However, that's really not useful if you have stuff like CSS or uncompiled TS you need to go to the network first and compile.
But if you use Bun.serve() and serve that HTML text content statically, behind the scenes Bun will automatically fetch all other linked resources the HTML needs, compiling typescript and doing all sorts of other build steps on the fly.
So if you have some static HTML like this that references CSS and TS:
<!DOCTYPE html>
<head>
<title>Home</title>
<link rel="stylesheet" href="./reset.css" />
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./sentry-and-preloads.ts"></script>
<script type="module" src="./my-app.tsx" /></script>
</body>
You can import it and then serve it statically via a route to HTML map through the static key option in the Bun.serve() options or through the routes key:
import homepage from "./index.html";
const server = Bun.serve({
static: {
"/": homepage,
},
async fetch(req) {
// ... api requests
},
});
import appHTML from "./frontend/index.html";
const server = Bun.serve({
port: 3000, // Optional; defaults to process.env.PORT || 3000
fetch(request: Request): Response | Promise<Response> {
return new Response("pAGE NOT FOUND!", {
status: 404,
});
},
routes: {
"/": appHTML,
},
});
You can use this to make a complete SSG app, as well as programmatically refreshing and reloading static pages through the server.reload() method:
import { serve } from "bun";
const server = serve({
static: {
"/": new Response("Static!"),
},
async fetch(request) {
return new Response("Dynamic!");
},
});
setInterval(() => {
const date = new Date().toISOString();
server.reload({
static: {
"/": new Response(`Static! Updated at ${date}`),
},
});
}, 1000);
Complete modern bun server
import { sql, serve } from "bun";
import dashboard from "./dashboard.html";
import homepage from "./index.html";
const server = serve({
routes: {
// ** HTML imports **
// Bundle & route index.html to "/". This uses HTMLRewriter to scan the HTML for `<script>` and `<link>` tags, run's Bun's JavaScript & CSS bundler on them, transpiles any TypeScript, JSX, and TSX, downlevels CSS with Bun's CSS parser and serves the result.
"/": homepage,
// Bundle & route dashboard.html to "/dashboard"
"/dashboard": dashboard,
// ** API endpoints ** (Bun v1.2.3+ required)
"/api/users": {
async GET(req) {
const users = await sql`SELECT * FROM users`;
return Response.json(users);
},
async POST(req) {
const { name, email } = await req.json();
const [user] =
await sql`INSERT INTO users (name, email) VALUES (${name}, ${email})`;
return Response.json(user);
},
},
"/api/users/:id": async req => {
const { id } = req.params;
const [user] = await sql`SELECT * FROM users WHERE id = ${id}`;
return Response.json(user);
},
},
// Enable development mode for:
// - Detailed error messages
// - Hot reloading (Bun v1.2.3+ required)
development: true,
});
console.log(`Listening on ${server.url}`);
Websockets in Bun
Bun.serve() offers built in support for creating websockets, keeping track of clients, upgrade requests, etc.
For a complete guide, go here:
On an HTTP request to your server, you can listen on a specific route for websocket upgrade requests through the server.upgrade(req) method:
const upgraded = server.upgrade(req)
- This method tries to see if the client requesting a socket upgrade, and if not, it returns false. It returns true if the client is trying to upgrade to the websocket protocol.
Bun.serve({
websocket: {
open: (ws) => { console.log("Client connected"); },
message: (ws, message) => { console.log("Received:", message); },
close: (ws) => { console.log("Client disconnected"); },
},
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === "/chat") {
const upgraded = server.upgrade(req);
if (!upgraded) return new Response("Upgrade failed", { status: 400 });
}
return new Response("Hello World");
},
});
You have all possible websocket configuration through the websocket object in the server options:
open(ws): a callback that gets triggered when a new websocket connects on the client
Here is a complete example with type safety for messages:
type Payloads<
SendPayload extends Record<string, unknown>,
ReceivePayload extends Record<string, unknown>
> = {
sendMessagePayload: SendPayload;
receiveMessagePayload: ReceivePayload;
};
type AppSockets = {
"dogs": {
sendMessagePayload: {message: string},
receiveMessagePayload: {message: string},
}
}
In the example below, we listen for websocket upgrade requests on the /websocket route.
import appHTML from "./frontend/index.html";
// class for type safety
class BunServerSocket<T extends Record<string, Payloads<any, any>>> {
constructor(public ws: Bun.ServerWebSocket<unknown>) {}
sendMessage<K extends keyof T>(key: K, message: T[K]["sendMessagePayload"]) {
this.ws.send(JSON.stringify({ type: key, payload: message }));
}
onMessage<K extends keyof T>(
key: K,
message: string,
listener: (payload: T[K]["receiveMessagePayload"]) => void
) {
try {
const data = JSON.parse(message) as {
type: K;
payload: T[K]["receiveMessagePayload"];
};
if (data.type === key) {
listener(data.payload);
}
} catch (e) {
console.error(`error tried to parse:`, message);
}
}
}
const server = Bun.serve({
port: 3000, // Optional; defaults to process.env.PORT || 3000
routes: {
"/": appHTML,
"/websocket": (req) => {
const upgraded = server.upgrade(req);
if (!upgraded) return new Response("Upgrade failed", { status: 400 });
},
},
websocket: {
open: (ws) => {
console.log(`Client connected`);
},
message: (ws, message) => {
console.log("Received:", message);
const websockets = new BunServerSocket<AppSockets>(ws);
websockets.sendMessage("dogs", {
message: "hello from server!",
});
websockets.onMessage("dogs", message as string, (payload) => {
console.log(payload.message);
});
},
close: (ws) => {
console.log("Client disconnected");
},
},
});
console.log(`Listening on http://localhost:${server.port}`);
You can then use the simple web standard for sockets on the client side:
const websocket = new WebSocket("ws://localhost:3000/websocket");
websocket.onopen = (e) => {
websocket.send(
JSON.stringify({
type: "dogs",
payload: {
message: "hello world",
},
})
);
};
websocket.onmessage = (e) => {
console.log(e.data);
};
Streaming files
You can easily stream files on the server-side as an API route by just sending back a response and passing in a BunFile iinstance:
Bun.serve({
fetch(req) {
return new Response(Bun.file("./hello.txt"));
},
});