Have you seen this new, cool, and fast runtime for JavaScript, and are wondering how to begin developing web applications? Maybe this article will help. I love to see new ways to create applications that bring innovation to the JS ecosystem, and Bun brings a little more to it. Here, without further libraries, you can create your API, test it, bundle it, and even use its own SQLite integration, all of that while being a fast and easy-to-use runtime. It even has some frameworks already, but it's content for the future.
Installation and Hello World
First of all, download and install bun using curl just like said in bun.sh
☁ ~ curl -fsSL 'https://bun.sh/install' | bash
######################################################################## 100,0%
bun was installed successfully to ~/.bun/bin/bun
Run 'bun --help' to get started
Then, create a folder where you want your project to be, cd into it, and execute bun init
, this will scaffold a new project, you'll have to choose the project's name and the entry point. By default the cli will use the name of your folder and start at index.ts.
☁ projects mkdir bunApp
☁ projects cd bunApp
☁ bunApp bun init
bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit
package name (bunapp):
entry point (index.ts):
Done! A package.json file was saved in the current directory.
+ index.ts
+ .gitignore
+ tsconfig.json (for editor auto-complete)
+ README.md
To get started, run:
bun run index.ts
☁ bunApp
After that, open your favorite ide(here im using vscode) and you'll see a very lean content, with some configuration files and a index.ts
containing our Hello World!!
Almost all of those files are really common to every repository, but there's one called bun.lockb
, its an auto-generated file similar to others .lock
files and that's not of so much importance right now, but you can learn about it in the Bun documentation.
We already can run our index.ts
file to start our little project,
☁ bunApp bun index.ts
Hello via Bun!
☁ bunApp
Before we continue to the next topic, there's one more thing to do. If you're familiar with Node, you've probably used Nodemon to monitor the project and reload when the code is changed. Bun simply uses the --watch
tag to run in this mode, so you don't need an external module.
Let's add two scripts to our package.json, one to start the project and one for the developer mode, including the --watch
tag.
{
"name": "bunapp",
"module": "index.ts",
"type": "module",
"scripts": {
"start": "bun run index.ts",
"dev": "bun --watch run index.ts"
},
"devDependencies": {
"bun-types": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
}
Routes
To initiate our server we just use Bun.serve()
. It can receive some parameters, but for now we only need the port
to access our application and the fetch()
handler which we're using to deal with our requests. Write the code below and run our script bun run dev
const server = Bun.serve({
port: 8080,
fetch(req) {
return new Response("Bun!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)
That's our first http request. As we didn't specified the method, it will return the response Bun!
to any request made for our endpoint, which should be localhost:8080
. We will deal with it in the next topic, for now lets just add some more code following the documentation example to compose our routes.
const server = Bun.serve({
port: 8080,
fetch(req) {
const url = new URL(req.url)
if(url.pathname === "/") return new Response("Home Page!")
if(url.pathname === "/blog") return new Response("Blog!")
return new Response("404!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)
It's taking our url from the request object and parsing to a URL object using Node's API. It happens because Bun aims for Node full compatibility, so most of libs and packages used on Node works on Bun out of the box.
HTTP Requests
If you wish, use console.log(req)
to view our request object, it looks like this:
Listening on localhost: 8080...
Request (0 KB) {
method: "GET",
url: "http://localhost:8080/",
headers: Headers {
"host": "localhost:8080",
"connection": "keep-alive",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"accept-language": "en-US,en",
"sec-fetch-mode": "navigate",
"sec-fetch-dest": "document",
"accept-encoding": "gzip, deflate, br",
"if-none-match": "W/\"b-f4FzwVt2eK0ePdTZJcUnF/0T+Zw\"",
"sec-ch-ua": "\"Brave\";v=\"117\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"117\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Linux\"",
"sec-gpc": "1",
"sec-fetch-site": "none",
"sec-fetch-user": "?1"
}
}
Now here's the thing, we can use a LOT of conditionals to check the method and/or the endpoint. It becomes painfully polluted and doesn't look good to read
const server = Bun.serve({
port: 8080,
fetch(req) {
const url = new URL(req.url)
if(url.pathname === "/") return new Response("Home Page!")
if(url.pathname === "/blog") {
switch (req.method) {
case "GET":
// handle with GET
case "POST":
// handle with POST
case "PUT":
// handle with PUT
case "DELETE":
// handle with DELETE
}
// any other routes and methods...
}
return new Response("404!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)
Remember that most Node packages runs on Bun as well? Let's use Express to ease our development process, we can view details in Bun's documentation. Let's start by stopping our application with CTRL + C
and running bun add express
Listening on localhost: 8080...
^C
☁ bunApp bun add express
bun add v1.0.2 (37edd5a6)
installed express@4.18.2
58 packages installed [1200.00ms]
☁ bunApp
and rewrite our index.ts
using a Express template with some routes
import express, { Request, Response } from "express";
const app = express();
const port = 8080;
app.use(express.json());
app.post("/blog", (req: Request, res: Response) => {
//create new blog post
});
app.get("/", (req: Request, res: Response) => {
res.send("Api running");
});
app.get("/blog", (req: Request, res: Response) => {
//get all posts
});
app.get("/blog/:post", (req: Request, res: Response) => {
//get a specific post
});
app.delete("/blog/:post", (req: Request, res: Response) => {
//delete a post
});
app.listen(port, () => {
console.log(`Listening on port ${port}...`);
});
Adding a database
One last thing for our CRUD is to implement Database connections. Bun already has its own SQLite3 driver, but we're using Prisma since dealing with an ORM is easier. Let's follow the guide from Bun documentation and start by adding Prisma with bun add prisma
and initializing it with bunx prisma init --datasource-provider sqlite
. Then navigate to our new schema.prisma
file and insert a new model.
☁ bunApp bun add prisma
bun add v1.0.2 (37edd5a6)
installed prisma@5.3.1 with binaries:
- prisma
2 packages installed [113.00ms]
☁ bunApp bunx prisma init --datasource-provider sqlite
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Run prisma db pull to turn your database schema into a Prisma schema.
3. Run prisma generate to generate the Prisma Client. You can then start querying your database.
More information in our documentation:
https://pris.ly/d/getting-started
☁ bunApp
After that, run bunx prisma generate
and then bunx prisma migrate dev --name init
. Now we got what we need to our little API. Go back to our index.ts
, import and initialize the Prisma client, then we're ready to finish our routes.
import { PrismaClient } from "@prisma/client";
/* Config database */
const prisma = new PrismaClient();
The final index.ts
file should look like this in the end:
import express, { Request, Response } from "express";
import { PrismaClient } from "@prisma/client";
/* Config database */
const prisma = new PrismaClient();
/* Config server */
const app = express();
const port = 8080;
app.use(express.json());
app.post("/blog", async (req: Request, res: Response) => {
try {
const { title, content } = req.body;
await prisma.post.create({
data: {
title: title,
content: content,
},
});
res.status(201).json({ message: `Post created!` });
} catch (error) {
console.error(`Something went wrong while create a new post: `, error);
}
});
app.get("/", (req: Request, res: Response) => {
res.send("Api running");
});
app.get("/blog", async (req: Request, res: Response) => {
try {
const posts = await prisma.post.findMany();
res.json(posts);
} catch (error) {
console.error(`Something went wrong while fetching all posts: `, error);
}
});
app.get("/blog/:postId", async (req: Request, res: Response) => {
try {
const postId = parseInt(req.params.postId, 10);
const post = await prisma.post.findUnique({
where: {
id: postId,
},
});
if (!post) res.status(404).json({ message: "Post not found" });
res.json(post);
} catch (error) {
console.error(`Something went wrong while fetching the post: `, error);
}
});
app.delete("/blog/:postId", async (req: Request, res: Response) => {
try {
const postId = parseInt(req.params.postId, 10);
await prisma.post.delete({
where: {
id: postId,
},
});
res.send(`Post deleted!`);
} catch (error) {
return res.status(404).json({ message: "Post not found" });
}
});
app.listen(port, () => {
console.log(`Listening on port ${port}...`);
});
Conclusion
Finally our API is done! From here on you can implement a bunch of other things such as: adding more models, creating a frontend and, of course, writing some tests(try do that next). You can see that Bun provides some quality of life while being compatible with most of Node packages, making our developer lives easier.
Top comments (6)
Awesome article! I didn't had time to read about this Bun fever until now.
Thanks for the insights!
Loved how you baby steps through the process. It's nice to have Node modules from the get go. Do you have any benchmark data agains other runtime options?
I do not have an in-depth benchmark. You can see some comparisons on Bun's homepage, but I've heard that it's faster for some and slower for others, depending on how you use it.
Amazing explanation, I've read about Bun but didn't have time to see any code yet, is great to know how it works
I am confused. First you use the bun.Serve() which is the whole reason why Bun is fast (because it's using uWebsockets under the hood). Then you switch to using the "regular" app.listen from Express which makes it A LOT slower. Then why use Bun at all? You even chose to use an ORM over the 10x faster built-in SQLite3 library of Bun...
👏🏻👏🏻👏🏻