Build a fast, Next.js-like app with Bun
Tags: nextjs, webdev
Published: 2023-08-23 13:58:42

**Written by [Clara Ekekenta](https://blog.logrocket.com/author/claraekekenta/)✏️**

In this tutorial, we’ll use the [Bun Bundler](https://bun.sh/blog/bun-bundler) to create a fast, Next.js-like blog application with server-side rendering (SSR) and client-side hydration. We’ll also explore Bun’s new [JavaScript Macros](https://bun.sh/blog/bun-macros) feature, which is part of the tighter integration that Bun aims for between its bundler and runtime to boost speed. 

Let’s get started! 

_Jump ahead:_

*   [Prerequisites](#prerequisites)
*   [What is Bun?](#what-is-bun)
*   [What is Bun Bundler?](#what-is-bun-bundler)
*   [Manually bundling a project with Bun](#manually-bundling-project-bun)
    *   [Setting up the environment](#setting-up-environment)
    *   [Setting up the Node.js project](#setting-up-node-js-project)
    *   [Creating the application files](#creating-application-files)
    *   [Bundling the application](#bundling-application)
    *   [Running the application](#running-application)
*   [Automatically bundling a project with Bun](#automatically-bundling-project-bun)
    *  [Creating a new Bun project](#creating-new-bun-project)
    *   [Understanding SSR and hydration](#understanding-ssr-hydration)
    *   [Creating pages](#creating-pages)
    *   [Creating a post page](#creating-post-page)
    *   [Adding interactivity](#adding-interactivity)
    *   [Adding styling](#adding-styling)
    *   [Using Bun Macros](#using-bun-macros)

## Prerequisites

To follow along with this tutorial, you’ll need the following:

*   Node.js v14 or later installed on your machine
*   npm; this is usually bundled with Node.js
*   CURL; you can install it with the following command: `sudo apt install curl`
*   A basic understanding of Typescript, React, and web development principles will be beneficial but is not required

## What is Bun?

[Bun is a sophisticated JavaScript runtime](https://blog.logrocket.com/getting-started-bun-react/#what-is-bun) that is equipped with inbuilt Web APIs, including Fetch and WebSockets, among many others. It incorporates [JavaScriptCore](https://developer.apple.com/documentation/javascriptcore), an engine renowned for its speed and memory efficiency, even though it's typically more challenging to embed compared to popular engines like V8. 

Bun is designed to expedite the JavaScript development process to unprecedented speeds. As an all-inclusive tool, Bun doesn't just enhance compilation and parsing rates, it also comes with its own suite of tools for dependency management and bundling. This makes Bun a comprehensive, one-stop solution for developers looking to optimize their workflow and improve efficiency.

## What is Bun Bundler?

Bun Bundler is a fast native bundler that is part of the Bun ecosystem. It is designed to reduce the complexity of JavaScript by providing a unified plugin API that works with both the bundler and the runtime. This means any plugin that extends Bun's bundling capabilities can also be used to extend Bun's runtime capabilities. 

Bun Bundler is designed to be fast, with [benchmarks](https://github.com/oven-sh/bun/tree/main/bench/bundle) showing it to be significantly faster than other popular bundlers. It also provides a great developer experience, with an API designed to be unambiguous and unsurprising. 

Bun Bundler supports a variety of file types and module systems, and it has inbuilt support for tree shaking, source maps, and minification. It also has experimental support for React Server Components.

## Manually bundling a project with Bun <a name="manually-bundling-project-bun">

To get started with this tutorial, let’s walk through the process of setting up a project and manually bundling and running it with Bun.

### Setting up the environment <a name="setting-up-environment">

First, we’ll need to install Bun on our Linux machine. Let’s run the following command on the terminal:

```bash
curl -fsSL https://bun.sh/install | bash
```

![Installing Bun Linux Machine](http://blog.logrocket.com/wp-content/uploads/2023/08/installing-bun-linux-machine.png) Once installation is complete, we’ll run the following commands to add Bun to `$PATH` and confirm the build:

```bash
exec /bin/zsh 
bun --help 
```

### Setting up the Node.js project <a name="setting-up-node-js-project">

Next, we’ll need to set up our project environment. Let’s start by creating a new directory for our project and navigate into it:

```bash
mkdir bun-blog && cd bun-blog
```

Then, we’ll initialize a new Node.js project:

```bash
npm init -y
```

### Creating the application files <a name="creating-application-files">

For this tutorial, we'll start by creating a simple client-side rendered React app. We’ll create two files, `index.tsx` and `Blog.tsx`, and then add the following code to the `Blog.tsx` file:

```javascript
export function Blog(props: {title: string, content: string}) {
  return (
    <div>
      <h2>{props.title}</h2>
      <p>{props.content}</p>
    </div>
  );
}
```

Now, let’s import the the `Blog` function in the `index.tsx` file:

```javascript
import * as ReactDOM from 'react-dom/client';
import React from 'react';
import { Blog } from './Blog.tsx';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( <Blog title="My First Blog Post" content="This is the content of my first blog post." />)
```

### Bundling the application <a name="bundling-application">

Next, we’ll bundle our application using the following command:

```bash
bun build ./index.tsx --outdir ./out
```

The `bun build` command tells Bun to generate a new bundle from the `index.tsx` file and write it to the `./out` directory. The bundled file will be `./out/index.js`. 

Let’s create an `index.html` file in the `./out` directory to run the file with the code snippet below:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My App</title>
</head>
<body>
    <div id="root"></div>
    <script type="module" src="./index.js"></script>
</body>
</html>
```

### Running the application <a name="running-application">

To run our application, we’ll need to serve the `./out` directory. We can do this by using the `bunx serve` command, like so:

```bash
bunx serve out
```

To see the bundled app in action, visit `http://localhost:5000`.

## Automatically bundling a project with Bun <a name="automatically-bundling-project-bun">

Instead of manually creating the application files and bundling them as we did previously, we can leverage the `bun create` `react-ssr` command, which has been updated to use `Bun.build` under the hood. This will enable us to easily scaffold a new SSR project.

### Creating a new Bun project <a name="creating-new-bun-project">

To start, we’ll need to create a new Bun React server-side rendered project with the command below:

```bash
bun create react-ssr
```

Next, let’s navigate to the project folder and run the application:

```bash
cd react-ssr
bun install
bun run dev
```

After running the above command, the Bun application will run on `http://localhost:3000`: ![Bun Application Running Local Host](http://blog.logrocket.com/wp-content/uploads/2023/08/bun-application-running-localhost.png) Let’s take a look at the important files in this newly created project:

*   `dev.tsx`: This file is instrumental in the development process. It constructs a browser version of all pages via `Bun.build`. When the development server is active, it responds to incoming requests by rendering the corresponding page from the `pages` directory into static HTML. This HTML output includes a `<script>` tag that sources a bundled version of the `hydrate.tsx` file
*   `hydrate.tsx`: The primary role of this file is to reinvigorate the static HTML sent back by the server, ensuring a smooth and dynamic user experience on the frontend
*   `pages/*.tsx`: This directory comprises various pages that align with Next.js routing conventions; the system routes incoming requests based on the defined pages in this directory

### Understanding SSR and hydration <a name="understanding-ssr-hydration">

When we talk about modern web applications, two terms that frequently come up are server-side rendering and hydration. Let’s take a closer look to gain a better understanding:

*   **SSR**: In traditional web applications, rendering often takes place on the client side, but with SSR the server plays a more proactive role. When a user makes a request, the server pre-renders the page into HTML and sends this static HTML as a response. This results in faster initial page load times and better SEO performance. In our project, the `dev.tsx` file handles this process, ensuring that the appropriate page in the `pages` directory is converted to static HTML for incoming requests
*   **Hydration:** While SSR provides the initial speed, we don't want our app to remain static; we want it to be interactive. This is where hydration comes in. After SSR sends the static HTML page to the browser, the associated JavaScript (in our case, the `hydrate.tsx` file) runs to “hydrate" this static page, attaching event listeners and making it fully interactive. This creates a seamless transition from a static page to a dynamic app without reloading the browser

Together, SSR and hydration give us the best of both worlds — a fast initial load time with a rich, dynamic user experience.

### Creating pages

Bun adopts a file system-based routing approach, similar to Next.js. Here we’ll update the `react-ssr/pages/index.tsx` file to fetch some blogs from the [JSONPlaceholder API](https://jsonplaceholder.typicode.com). Then we’ll create another page to handle the creation of new blog posts. 

Let’s start by updating the `react-ssr/pages/index.tsx` file with the following code:

```javascript

import { useEffect, useState } from "react";
import { Layout } from "../Layout";
import { IBlog } from "../interface";

export default function () {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    async function getPosts() {
      const res = await fetch("https://jsonplaceholder.typicode.com/posts");
      const posts = await res.json();
      console.log(posts);
      setPosts(posts);
    }
    getPosts();
  }, []);
  return (
    <Layout title="Home">
      <div className="posts-container">
        <a href="/posts">Create New Post</a>
        {posts?.map((post: IBlog) => (
          <article key={post.id} className="post-article">
            <h2 className="post-title">{post.title}</h2>
            <p className="post-content">{post.body}</p>
          </article>
        ))}
      </div>
    </Layout>
  );
}
```

Here, `useEffect` fetches posts from the API whenever the `Home` component is mounted. The posts are stored in the component's local state and are displayed on the screen. This ensures that the displayed data is fresh every time the component is rendered, making it suitable for data that updates frequently. 

Now, let’s create an `IBlog` interface in the `react-ssr/interface` folder and add the following code:

```javascript
export interface IBlog {
    id: string;
    title: string;
    body: string;
}
```

### Creating a post page

To allow authors to create new posts, we’ll need to build a post page. Let’s create a `posts/index.tsx` file in the `react-ssr/pages/` folder, like so:

```javascript
import { Layout } from "../../Layout";
export default function () {
  return <Layout title="Create a new Post"></Layout>;
}
```

### Adding interactivity

To bring our blog to life, we’ll need to add some interactive elements. We'll start with a simple form on our `posts/index.tsx` page to gather information for new posts. Since we do not have a backend for this application, we’ll store the posts in the JSONPlaceholder API:

```javascript
import { useState } from "react";
import { Layout } from "../../Layout";

export default function () {
  const [title, setTitle] = useState("");
  const [body, setContent] = useState("");
  const handleSubmit = (e: { preventDefault: () => void }) => {
    e.preventDefault();
    // we'll handle the submission logic here later
  };
  return (
    <Layout title="Create a new Post">
      <div>
        <form onSubmit={handleSubmit}>
          <label>
            Title:
            <input
              type="text"
              value={title}
              onChange={(e) => setTitle(e.target.value)}
            />
          </label>
          <label>
            Content:
            <textarea
              value={body}
              onChange={(e) => setContent(e.target.value)}
            />
          </label>
          <button type="submit">Submit</button>
        </form>
      </div>
    </Layout>
  );
}
```

The above code defines a React functional component named `CreatePost`. It uses the `useState` Hook to manage local state for the `title` and `content` of a new post, initially setting both to an empty string. It also sets up a `handleSubmit` function to be used when the form is submitted, although we have not yet implemented the form submission. 

The component's render function returns a form with two input fields, for the title and content, and a submit button. The state of the `title` and `content` is linked to their respective input fields, with their state being updated whenever the input field value changes. 

Next, let’s update the `handleSubmit` function in the `posts/index.tsx` file to store the new post:

```javascript
...
import { IBlog } from 'interface/IBlog';
...
  const handleSubmit = async (e: { preventDefault: () => void; }) => {
    e.preventDefault();
    const id = Math.random().toString(36).substr(2, 9);
    const newPost: IBlog = { id, title, body };
    // Send a POST request to the JSONPlaceholder API
    const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      body: JSON.stringify(newPost),
      headers: {
        'Content-type': 'application/json; charset=UTF-8',
      },
    });
    const data = await res.json();
    console.log(data);
    setTitle("");
    setContent("");
  };
...
```

The `handleSubmit` function that we mentioned previously is an event handler for form submissions. It first prevents the default form event; then it generates a unique `id` and creates a new post with the `title` and `body` from the state. 

The new post is then sent to the JSONPlaceholder API via a `POST` request. The response from the server is logged to the console, and the form is reset by clearing the title and content states.

### Adding styling

Now we’ll add some styling to our blog application to make it visually appealing. Let’s update the `public/index.css` file to style all the components in the application, like so:

```css
.posts-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 2rem auto;
  padding: 1rem;
}

.post-article {
  width: 80%;
  margin-bottom: 2rem;
  border: 1px solid #ddd;
  border-radius: 10px;
  padding: 1rem;
  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
}

.post-title {
  font-size: 2rem;
  color: #333;
  margin-bottom: 1rem;
}

.post-content {
  font-size: 1rem;
  color: #666;
}
```

### Using Bun Macros

Bun Macros is a powerful feature in Bun that allows us to replace parts of our code with other code at build time. This can be useful for a variety of scenarios, such as optimizing performance, removing code that isn't needed in a particular build, or improving code readability. 

In our blog application, we‘ll use Bun Macros to replace the API URL with a local storage key when we're running tests. Not having to make actual API calls during testing will make our tests faster and more reliable. 

First, we’ll create a `macro.ts` file and then use the `createMacro` function from `bun.macro` to create the macro. In this tutorial, we'll create a macro that replaces the API URL with a local storage key:

```javascript
export const apiUrl = () => {
  if (process.env.NODE_ENV === 'test') {
    return 'localStorageKey';
  } else {
    return 'https://jsonplaceholder.typicode.com/posts';
  }
};
```

Now, we can use this macro in our `handleSubmit` function:

```javascript
...
import {apiURL} from './macro.ts' with { type: 'macro' }
... 
  const handleSubmit = async (e: { preventDefault: () => void; }) => {
    e.preventDefault();
    const id = Math.random().toString(36).substr(2, 9);
    const newPost: IBlog = { id, title, body };
    // Use the apiUrl macro
    const res = await fetch(apiUrl(), {
      method: 'POST',
      body: JSON.stringify(newPost),
      headers: {
        'Content-type': 'application/json; charset=UTF-8',
      },
    });
    const data = await res.json();
    setTitle("");
    setContent("");
  };
...
```

With this, our blog application is complete! We've used Bun Bundler to create a fast, Next.js-like blog application with server-side rendering and client-side hydration.

## Conclusion

In this tutorial, we demonstrated how to set up a project using Bun, create a simple application, bundle it using Bun Bundler, and serve it using Bun's inbuilt server. We also showed how to simplify the process of setting up a new project using the `bun create` command, which uses `Bun.build` under the hood. 

Bun Bundler is a powerful tool that can help reduce the complexity of your JavaScript projects and improve your development speed. Its integration with the Bun runtime and its support for a wide range of file types and module systems make it a versatile tool for any JavaScript developer. Whether you're building a simple client-side app or a complex full-stack application, Bun Bundler has the features and performance to meet your needs.

---
##Are you adding new JS libraries to improve performance or build new features? What if they’re doing the opposite?

There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.

[LogRocket](https://lp.logrocket.com/blg/javascript-signup) is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.

[![LogRocket Signup](https://blog.logrocket.com/wp-content/uploads/2019/10/errors-screenshot.png)](https://lp.logrocket.com/blg/javascript-signup)

[LogRocket](https://lp.logrocket.com/blg/javascript-signup) works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Build confidently — [Start monitoring for free](https://lp.logrocket.com/blg/javascript-signup).