esbuild is a fast and simple tool that helps you bundle JavaScript code. It’s built for speed and supports the latest JavaScript features, making it a great choice whether you’re working on a small project or a full-scale app.
In this article, you’ll learn how to set up esbuild and use its features to improve your JavaScript development workflow.
Prerequisites
Before you start, make sure you have Node.js version 20 or higher and npm installed on your computer. You should also have a basic understanding of JavaScript and how build tools work.
What is esbuild?
esbuild is a fast JavaScript bundler and minifier built by Evan Wallace, the co-founder of Figma. It’s known for its impressive speed, thanks to a few smart design choices:
- It’s written in Go, so it runs much faster than tools built in JavaScript
- It uses all your CPU cores to process files in parallel
- It works in a single pass with minimal file handling
- It has highly optimized code for parsing, printing, and minifying
- It generates machine code directly instead of relying on slower JavaScript layers
But esbuild isn’t just fast. It also gives you:
- Support for JavaScript and TypeScript
- Built-in JSX transformation
- Tree shaking to remove unused code
- CSS bundling
- Source map generation
- A plugin system to add custom features
Overall, esbuild focuses on speed and simplicity, making it much easier to use than many other build tools.
Getting started with esbuild
To understand how esbuild works, let’s walk through a simple example project. You’ll see how easy it is to set up and start using esbuild in your JavaScript workflow.
First, create a new folder for your project and initialize it with npm:
mkdir esbuild-demo && cd esbuild-demo
npm init -y
Now install esbuild as a development dependency:
npm install --save-dev esbuild
You’ll probably notice how fast the installation finishes—esbuild makes a strong first impression with its speed.
Next, set up a basic project structure with the following commands:
mkdir src
touch src/main.js
touch src/utils.js
Now add some simple code to these files to test your setup:
export function greet(name) {
return `Hello, ${name}!`;
}
export function getCurrentTime() {
const now = new Date();
return now.toLocaleTimeString();
}
This utility file includes two functions:
greet(name)
returns a friendly greeting.getCurrentTime()
returns the current time in a readable format.
Next, let's use these functions in your main JavaScript file.
import { greet, getCurrentTime } from './utils';
document.addEventListener('DOMContentLoaded', () => {
const app = document.getElementById('app');
const heading = document.createElement('h1');
heading.textContent = greet('esbuild user');
const timeDisplay = document.createElement('p');
timeDisplay.textContent = `Current time: ${getCurrentTime()}`;
app.appendChild(heading);
app.appendChild(timeDisplay);
console.log('Application initialized!');
});
This script waits for the page to load, then adds a heading and a paragraph to the page. The content comes from the utility functions you just wrote.
Now, create a basic HTML file in the root directory to load your bundled JavaScript:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>esbuild Demo</title>
<script src="dist/bundle.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
The HTML file includes a <div>
with the ID app
—where your JavaScript will add content. It also loads the bundled JavaScript file that you'll create with esbuild in the next step.
With your files set up, it’s time to bundle your code using esbuild. Open your package.json
and add these scripts:
{
"name": "esbuild-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "esbuild src/main.js --bundle --outfile=dist/bundle.js",
"dev": "esbuild src/main.js --bundle --outfile=dist/bundle.js --watch"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"esbuild": "^0.20.1"
}
}
Let's run the build command:
npm run build
> esbuild-demo@1.0.0 build
> esbuild src/main.js --bundle --outfile=dist/bundle.js
dist/bundle.js 15b
⚡ Done in 78ms
Even with a slightly larger file size or more features, the build time stays incredibly fast.
In this case, it finished in just 78 milliseconds. That kind of speed is one of the reasons developers love esbuild.
To see your app in action, open index.html
in your browser. You should see a heading that says "Hello, esbuild user!" and a line showing the current time.
With everything set up and running in the browser, you've seen how fast and easy it is to get started with esbuild.
But the real advantage shows up during active development, where quick rebuilds help you stay productive.
Development workflow with esbuild
esbuild really shines during development, especially with its watch mode.
This feature automatically rebuilds your project whenever you make changes to your files, so you don’t have to run the build command manually each time.
npm run dev
> esbuild-demo@1.0.0 dev
> esbuild src/main.js --bundle --outfile=dist/bundle.js --watch
[watch] build finished, watching for changes...
Now, build will watch your files and rebuild them automatically when they change. Let's modify src/utils.js
:
export function greet(name) {
const hour = new Date().getHours();
let greeting = '';
if (hour < 12) {
greeting = 'Good morning';
} else if (hour < 18) {
greeting = 'Good afternoon';
} else {
greeting = 'Good evening';
}
}
export function getCurrentTime() {
const now = new Date();
return now.toLocaleTimeString();
}
As soon as you save, you'll see esbuild instantly rebuild:
[watch] build finished, watching for changes...
[watch] build started (change: "src/utils.js")
[watch] build finished
Refresh your browser, and you'll see the updated text:
Other bundlers often take several seconds or more to rebuild after changes. With esbuild, updates happen almost instantly. This quick feedback helps you stay focused and makes development smoother, especially as your project grows.
Working with ESM format
Modern JavaScript relies heavily on ECMAScript Modules (ESM). esbuild supports ESM out of the box, making it perfect for contemporary web development. Let's update your project to use proper ESM format.
First, update your HTML file to load JavaScript as an ES module:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>esbuild Demo</title>
<script type="module" src="dist/bundle.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
The key change is adding type="module"
to the script tag, telling the browser to treat your bundle as an ES module.
Now update your build script in package.json
to explicitly output ESM format:
"scripts": {
"build": "esbuild src/main.js --bundle --outfile=dist/bundle.js --format=esm",
"dev": "esbuild src/main.js --bundle --outfile=dist/bundle.js --format=esm --watch"
}
The --format=esm
flag ensures esbuild generates a proper ES module bundle.
Let's now add a simple module to demonstrate ESM's organization benefits. Create a new file for UI helpers:
// Simple helper functions for UI elements
export function createButton(text, clickHandler) {
const button = document.createElement('button');
button.textContent = text;
button.addEventListener('click', clickHandler);
return button;
}
export function addStyles(element, styles) {
Object.assign(element.style, styles);
return element;
}
In this module, createButton
helps you easily make a button with a click event, and addStyles
lets you apply inline styles using a plain JavaScript object. These small utilities help keep your main code cleaner and more focused.
Now update your main.js
to use these helpers:
import { greet, getCurrentTime } from './utils';
import { createButton, addStyles } from './ui';
document.addEventListener('DOMContentLoaded', () => {
const app = document.getElementById('app');
// Create heading
const heading = document.createElement('h1');
heading.textContent = greet('esbuild user');
// Create time display
const timeDisplay = document.createElement('p');
timeDisplay.textContent = `Current time: ${getCurrentTime()}`;
timeDisplay.id = 'time-display';
// Create refresh button using our UI module
const refreshButton = createButton('Refresh Time', () => {
timeDisplay.textContent = `Current time: ${getCurrentTime()}`;
});
// Add some simple styles
addStyles(heading, {
color: '#3b82f6',
marginBottom: '0.5rem'
});
addStyles(timeDisplay, {
color: '#4b5563',
fontWeight: 'bold'
});
// Add everything to the page
app.appendChild(heading);
app.appendChild(timeDisplay);
app.appendChild(refreshButton);
console.log('Application initialized with ESM!');
});
Here, you’re importing utility functions from separate modules to build and style the UI. This keeps your main script focused on app logic while offloading repetitive tasks to reusable helpers—one of the key benefits of using ES modules.
To avoid CORS errors when working with ES modules locally, you need a web server:
npm install --save-dev serve
Then update your package.json
scripts:
"scripts": {
"build": "esbuild src/main.js --bundle --outfile=dist/bundle.js --format=esm",
"dev": "esbuild src/main.js --bundle --outfile=dist/bundle.js --format=esm --watch",
"serve": "serve ."
}
Run the build and serve commands to bundle your code and start a local server(:
npm run build
npm run serve
Visit http://localhost:3000
to see your updated app with the refresh button:
This simplified approach shows how ES modules make it easier to organize your code into clear, focused files. Each module handles a specific task, which keeps your codebase clean and easier to manage as your project grows, without adding unnecessary complexity.
Adding styles with esbuild
Now that your project is working with ES modules, let's enhance it with proper CSS styling. esbuild can handle CSS files directly alongside your JavaScript.
Create a new file called src/styles.css
:
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f7f8fa;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
h1 {
color: #3b82f6;
margin-bottom: 1rem;
}
p {
margin-bottom: 1.5rem;
}
button {
background-color: #3b82f6;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
transition: background-color 0.2s;
}
button:hover {
background-color: #2563eb;
}
#time-display {
font-weight: 500;
color: #4b5563;
}
Now, modify your main.js
file to import this CSS file:
import './styles.css';
import { greet, getCurrentTime } from './utils';
import { createButton, addStyles } from './ui';
document.addEventListener('DOMContentLoaded', () => {
const app = document.getElementById('app');
// Create heading
const heading = document.createElement('h1');
heading.textContent = greet('esbuild user');
// Create time display
const timeDisplay = document.createElement('p');
timeDisplay.textContent = `Current time: ${getCurrentTime()}`;
timeDisplay.id = 'time-display';
// Create refresh button using our UI module
const refreshButton = createButton('Refresh Time', () => {
timeDisplay.textContent = `Current time: ${getCurrentTime()}`;
});
// No need for inline styles anymore, using CSS file instead
// Add everything to the page
app.appendChild(heading);
app.appendChild(timeDisplay);
app.appendChild(refreshButton);
console.log('Application initialized with CSS and ESM!');
});
Notice the key change at the top: import './styles.css';
. With this simple import, esbuild will automatically handle the CSS file. You also don't need the inline styles anymore since you have a proper CSS file.
Then, update your HTML to include the CSS file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>esbuild Demo</title>
<link rel="stylesheet" href="dist/bundle.css">
<script type="module" src="dist/bundle.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
Rerun the build command:
npm run build
> esbuild-demo@1.0.0 build
> esbuild src/main.js --bundle --outfile=dist/bundle.js --format=esm
dist/bundle.js 1.3kb
dist/bundle.css 595b
⚡ Done in 15ms
When you refresh your browser, you'll see the updated styles(make sure the server is still running):
Notice that esbuild now outputs two files: one for JavaScript and one for CSS. This approach can be better for production as it allows the browser to cache them separately.
Final thoughts
esbuild is a fast and minimal bundler that’s great for quick setups and learning how modern JavaScript builds work. It keeps things simple while delivering impressive speed.
For more features like hot reloading and a smoother dev experience, consider Vite. It builds on esbuild and adds everything you need for larger, more dynamic projects.
Check out this article on getting started with Vite to explore further.
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github