DEV Community

Cover image for Setting Up a Modern TypeScript Project with esbuild (No Framework)
Robin Viktorsson
Robin Viktorsson

Posted on • Originally published at Medium

1

Setting Up a Modern TypeScript Project with esbuild (No Framework)

This article was originally published on Medium.

Modern web development focuses on building fast, scalable, and maintainable applications — and choosing the right tools is essential to achieving that. If you’re working with TypeScript, you’re already prioritizing structure, type safety, and maintainability. But to transform your TypeScript code into a fully functioning, production-ready web application, you need more than just the TypeScript compiler (tsc) — you need a bundler. This is where esbuild comes in.

While the TypeScript compiler does an excellent job of transpiling your TypeScript into JavaScript, it doesn’t handle bundling, asset optimization, or other features that streamline both development and production workflows. esbuild steps in to fill these gaps by acting as a fast and efficient bundler that understands your project’s structure, optimizes assets, and produces performant output suitable for the browser.

In this guide, we’ll walk through creating a TypeScript project from scratch using esbuild as the bundler — without relying on any frameworks. You’ll learn how to compile TypeScript, serve files locally with live reloading, handle assets like CSS and images, and optimize your build for production with features such as tree shaking and minification.

Whether you’re diving into bundlers for the first time, building a lightweight utility, or laying the groundwork for a larger application, this hands-on tutorial will give you the confidence to leverage esbuild’s speed and efficiency to optimize your development and production workflows.

Sit back, grab some popcorn 🍿, and enjoy the read!


What is esbuild (as a Bundler)? 🤔

When using the TypeScript compiler (tsc), your code is compiled into individual .js files without bundling. While this maintains project organization, it doesn't optimize performance. Modern applications require a bundler to consolidate code, resolve dependencies, and optimize bundles, improving load times and performance. Bundlers also enable techniques like tree-shaking and dead code elimination (CDE), which remove unused code, further reducing file size. Additionally, they ensure compatibility across various module formats and environments and can handle assets like CSS and images. Features like code splitting and hot module replacement (HMR) make bundlers indispensable for optimizing both development and production workflows.

esbuild is an ultra-fast JavaScript bundler and minifier, designed to be both powerful and simple. The key to esbuild’s speed lies in its implementation. Unlike other bundlers written in JavaScript, esbuild is written in Go, a compiled systems language. This allows it to leverage parallelism and low-level optimizations that result in builds that are often 10–100x faster than tools like Webpack or Rollup.

At its core, esbuild is a bundler — meaning it takes multiple source files (JavaScript, TypeScript, JSX, etc.), resolves their dependencies, and combines them into optimized output files for the browser or Node.js. But esbuild goes beyond simple bundling by offering a complete toolchain for modern frontend development. This process includes key features such as:

  • Tree-Shaking: esbuild has built-in support for tree-shaking, automatically removing unused code from the final bundle. This results in smaller and more efficient output files — especially valuable in large TypeScript projects where minimizing bundle size directly improves load performance and runtime efficiency.

  • Asset Management: While esbuild primarily focuses on JavaScript, TypeScript, and CSS, it can be extended to handle static assets like images and fonts via plugins or external tools. With the right setup, esbuild can efficiently include and optimize these assets in your build pipeline, making it a flexible option for production-ready applications.

  • Plugins and Extensibility: esbuild supports a lightweight plugin API, allowing you to customize how files are processed. Although its plugin ecosystem is still growing compared to other bundlers (e.g. Webpack), esbuild covers many common needs out of the box, such as TypeScript transpilation, JSX transformation, and minification, without requiring complex configurations.

  • Development Experience: Thanks to its blazing speed, esbuild significantly improves the development experience. It can rebuild large projects in milliseconds, enabling rapid feedback loops. While esbuild doesn’t provide built-in Hot Module Replacement (HMR), it integrates well with tools like Vite or custom dev servers that offer live reloading and HMR support.

Overall, esbuild’s modern architecture and focus on speed make it a great choice for bundling TypeScript projects, especially when quick builds and simple configuration are key. While it may lack some of the deep customization and mature ecosystem of Webpack and Vite, its performance, simplicity, and native TypeScript support make it ideal for many modern web development workflows.


Creating a TypeScript Project with esbuild 👨‍💻

Creating a TypeScript project with esbuild is a great way to learn the modern JavaScript build process, optimize your applications, and gain hands-on experience with fast, lightweight tooling. While many web frameworks offer built-in TypeScript and bundling configurations, there are plenty of scenarios where a minimal, framework-free approach is more appropriate.

Whether you’re building a command-line utility, developing a modular JavaScript/TypeScript library, or simply experimenting with esbuild’s blazing-fast capabilities, this guide will walk you through setting up a TypeScript project from scratch using esbuild as the bundler.

We’ll cover everything from initializing a Node.js project to configuring esbuild, compiling TypeScript, running a lightweight development server, and preparing your project for production. With esbuild and TypeScript, you can create a lean, efficient, and production-ready build system — all while maintaining complete control over your development workflow.

1. Prerequisites 🧱

Ensure you have the following installed:

  • Node.js
  • npm (comes with Node.js)

2. Project Structure 📁

Let’s begin by setting up the foundational structure of our project. We’ll initialize a new Node.js project and generate a package.json file with default values using the npm init -y command. Continue to create the necessary directories and files for our TypeScript and esbuild setup:

// Note: 'touch' is not available in Windows, instead create file manually

mkdir ts-esbuild-app
cd ts-esbuild-app
npm init -y

# Create source and assets directory alongside its files
mkdir src
cd src
touch index.html
touch index.ts
touch styles.css
touch assets.d.ts

mkdir assets
cd assets
touch logo.png 

# Create output directory and index.html
cd ../..
mkdir dist
Enter fullscreen mode Exit fullscreen mode

After running the commands, your project directory should look like this:

ts-esbuild-app/
├── dist/
├── src/
│   └── index.ts
│   └── index.html
│   └── assets.d.ts
│   └── styles.css
│   ├── assets/
│   │   └── logo.png
├── package.json
Enter fullscreen mode Exit fullscreen mode

This structure separates your source code (src/) from your build output (dist/), which is a best practice in most modern web projects. We'll populate and refine the contents of all files in the upcoming sections as we configure TypeScript and esbuild.

3. Install Dependencies 📦

Now that we have our project structure in place, it’s time to install the essential development tools that will allow us to compile TypeScript and bundle our project with esbuild. Run the following command to install the required dependencies as development-only packages:

npm install --save-dev typescript esbuild live-server @types/node
Enter fullscreen mode Exit fullscreen mode

Here’s an overview of the purpose of each of these packages:

  • esbuild: The core bundler that takes your files and dependencies (JS, CSS, images, etc.) and outputs optimized bundles.

  • live-server: Serves your app locally with live reloading and Hot Module Replacement (HMR) at http://127.0.0.1:8080 or your configured port.

  • typescript: The TypeScript compiler (tsc) for writing .ts files and compiling them to JavaScript.

These are added as dev dependencies (--save-dev) because they’re only needed during development and build time—not at runtime. With these tools, you now have a full TypeScript + esbuild setup: write in TypeScript, compile and bundle with esbuild, and develop locally with live reloading (if you installed live-server).

4. Create tsconfig.json ✍️

To configure how TypeScript compiles your project, you need to create a tsconfig.json file. This file serves as the central configuration for the TypeScript compiler (tsc), allowing you to define the compiler options, project structure, and file inclusion patterns. To generate this file, run the following command in your project root:

npx tsc --init
Enter fullscreen mode Exit fullscreen mode

This command creates a tsconfig.json file with a default set of compiler options. You can then customize it to fit the needs of your project. Here’s a recommended configuration for a modern TypeScript + esbuild setup:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "ESNext",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  },
  "include": ["src"]
}
Enter fullscreen mode Exit fullscreen mode

The tsconfig.json file contains various compiler options that guide TypeScript in how it should compile your code.

  • "target": "ESNext" tells TypeScript to compile your code down to latest available ECMAScript JavaScript version. This ensures modern language features are supported while maintaining reasonable browser compatibility.

  • "module": "ESNext" instructs TypeScript to output native ES module syntax (import/export). This is necessary for esbuild to perform advanced bundling techniques like tree-shaking.

  • "moduleResolution": "node" uses Node.js-style module resolution, which is compatible with most bundling and package systems. This means TypeScript will mimic how Node.js resolves import paths.

  • "outDir": "./dist" specifies the directory where compiled JavaScript files should be emitted. In this setup, all output files will go to the dist folder.

  • "esModuleInterop": true enables interoperability between CommonJS and ES Module systems. This allows you to seamlessly import packages that use module.exports (CommonJS), such as many Node.js packages.

  • "strict": true enables all of TypeScript’s strict type-checking options. This helps catch potential bugs early and enforces best practices in your codebase.

  • "skipLibCheck": true skips type checking of external declaration files (e.g., from node_modules). This speeds up compilation and avoids unnecessary type conflicts from third-party packages.

  • "include": ["src"] tells TypeScript to only include and compile files located in the src folder. This helps keep the build focused and fast by excluding unnecessary files.

These settings work together to create an efficient, modern, and maintainable TypeScript development environment. They ensure compatibility with esbuild, optimize your build speed, and enable strong typing throughout your codebase.

5. Create esbuild.config.ts ⚙️

Now that we’ve set up TypeScript, it’s time to configure esbuild — the tool responsible for bundling your code and preparing it for the browser. Create a new file called esbuild.config.ts in the root of your project. This file will define how esbuild handles your source files, compiles TypeScript, and serves your application during development. Here’s a minimal yet effective configuration:

import esbuild from 'esbuild';
import path from 'path';

// Define the output directory for your build
const outDir = path.resolve(__dirname, 'dist');

// Create the esbuild build configuration
esbuild.build({
  entryPoints: ['src/index.ts'], // Main entry point (change as needed)
  bundle: true, // Enable bundling
  format: 'esm',
  outfile: path.join(outDir, 'index.js'), // Output file
  sourcemap: true, // Generate sourcemaps
  minify: true, // Minify the output for production
  assetNames: 'assets/[name]-[hash]', // Asset file naming convention with hash for cache busting
  loader: {
    '.png': 'file', // Handle PNG images
    '.jpg': 'file', // Handle JPEG images
    '.css': 'css', // Handle CSS files
    '.ts': 'ts', // Handle TypeScript files (optional, as esbuild does this by default)
  },
  external: [], // Exclude specific modules from being bundled (e.g., node_modules)
  define: {
    'process.env.NODE_ENV': '"production"', // Define environment variables for optimization
  }
}).catch(() => process.exit(1)); // Exit on error
Enter fullscreen mode Exit fullscreen mode

The esbuild.config.ts file is a configuration file used by esbuild, a lightning-fast JavaScript and TypeScript bundler. It defines how esbuild should process, bundle, and optimize your project’s source files, including JavaScript, TypeScript, CSS, and other assets. Through this configuration, you can customize the build process by specifying entry points, output formats, target environments, external dependencies, and plugin integrations. While esbuild favors simplicity and minimal configuration, the config file allows for advanced customization when needed, especially for large or complex projects.

The importance of the esbuild.config.ts file lies in its ability to deliver extremely fast builds with powerful customization. It enables you to fine-tune how files are compiled and bundled, optimize your output for production with built-in minification, and define build-time constants using environment variables. With support for plugins and watch mode, you can extend esbuild’s capabilities and enhance the development experience. Whether you're building a small library or a large application, esbuild.config.ts provides the flexibility and speed to efficiently manage your build process.

6. Create Sample Code 💻

Let’s add a small piece of TypeScript to verify that everything is wired up correctly. In the src/index.ts file, add the following simple function that logs a personalized greeting to the console:

import './styles.css'; // We will populate this file in next sub-chapter
import logo from './assets/logo.png'; // Will throw a TypeScript error which we will resolve later

function greet(name: string): void {
  const heading = document.createElement('h1');
  heading.textContent = `Hello, ${name}!`;
  document.body.appendChild(heading);

  const img = document.createElement('img');
  img.src = logo;
  img.alt = 'Project Logo';
  img.style.width = '200px';

  document.body.appendChild(img);
}

greet('TypeScript + esbuild');
Enter fullscreen mode Exit fullscreen mode

This minimal example serves as a sanity check to ensure TypeScript compiles properly and the esbuild bundler is functioning as expected.

You will get an TypeScript error “Cannot find module ‘./assets/logo.png’ or its corresponding type declarations.ts(2307)”. Ignore this for now. We will resolve it in the upcoming chapter 8. Bundle Images.

Next, let’s create a basic HTML file to load the compiled JavaScript bundle into the browser. In the src/index.html file, add the following code:

This HTML page simply loads the `index.js` file that esbuild will generate after compilation. Once the esbuild live server is running, this file will be served at `http://127.0.0.1:8080`, and the greeting should appear in your browser's developer console.

With just these two files — `index.ts` and `index.html` — you now have a basic but working TypeScript + esbuild setup ready for further development.
Enter fullscreen mode Exit fullscreen mode

7. Add and Bundle CSS 🎨

Now that your TypeScript and esbuild setup is working, let’s take it a step further and add support for CSS. esbuild can also bundle stylesheets, making it easy to manage CSS alongside your TypeScript files. Add an src/styles.css file which contains some simple styling:

body {
  background-color: #f0f0f0;
  font-family: sans-serif;
  padding: 2rem;
}

h1 {
  color: #3366ff;
}
Enter fullscreen mode Exit fullscreen mode

This works because you added this loader in esbuild.config.ts:

loader: {
    '.png': 'file',
    '.css': 'css',
},
Enter fullscreen mode Exit fullscreen mode

esbuild handles .css files directly if you use the loader: { '.css': 'css' } configuration. CSS is bundled into your final output, or injected depending on your preference.

8. Bundle Images 🖼️

To include images in your TypeScript + esbuild project, you need to configure esbuild to handle static assets like .png, .jpg, .svg, and more. We already created a logo.png while setting up the folder structure.

import logo from './assets/logo.png';
Enter fullscreen mode Exit fullscreen mode

However, our import of the logo.png file in index.ts is still throwing this error: “Cannot find module ‘./assets/logo.png’ or its corresponding type declarations.ts(2307)”. To solve this, add the following to assets.d.ts:

declare module '*.png' {
    const value: string;
    export default value;
}
Enter fullscreen mode Exit fullscreen mode

To make it available in the final dist/ folder, we’ll add the following parameter to out build script --loader:.png=file, but this is something we will add in the next chapter, so hold your horses for now.

When you run the build, your image (e.g., logo.png) will be available in the output folder, and your code will reference it correctly thanks to the static module declaration. If you're using something like live-server to serve your files, it will serve the image alongside your HTML and JavaScript, ensuring it displays properly in the browser.

9. Add Scripts in package.json 🔨

In the package.json file, you can define custom scripts to automate common development tasks. These scripts serve as shortcuts for commands you’d otherwise need to run manually in the terminal. When working with esbuild, you can set up similar build and start scripts to handle both development and production workflows.

Start by adding the following entries under the "scripts" section in your package.json:

"scripts": {
  "build": "esbuild --bundle src/index.ts --minify --loader:.png=file src/index.html --loader:.html=copy --outdir=dist",
  "start": "npm run build && live-server dist"
}
Enter fullscreen mode Exit fullscreen mode
  • The build script runs esbuild to bundle your TypeScript project. It takes the src/index.ts file as the entry point, bundles it along with its dependencies, outputs it to the dist/ folder as index.js, and minifies the output for production use. You can also configure this further using an esbuild.config.ts file for more advanced setups.

  • The start script uses live-server to serve the content from your dist/ folder at http://127.0.0.1:8080 by default. Unlike Webpack’s Dev Server, esbuild doesn’t include a built-in dev server with Hot Module Replacement (HMR). However, live-server provides automatic reloading whenever files in the dist/ folder change, giving you a simple and effective way to preview changes during development.

These scripts make it easy to build and serve your project without needing to remember long terminal commands, improving your development experience and keeping your workflow efficient.

10. Run the Project ▶️

Once you’ve added the scripts to your package.json, you’re ready to start developing your project using esbuild along with a lightweight development server. To launch the server and view your project in the browser, simply run the following command in your terminal:

npm start
Enter fullscreen mode Exit fullscreen mode

This command starts live-server, which serves your dist/ folder and opens the project in your default web browser at http://127.0.0.1:8080 (or another available port if that one’s taken).

C:\Users\Robin\ts-esbuild-app>npm run start

> ts-esbuild-app@1.0.0 start
> npm run build && live-server dist


> ts-esbuild-app@1.0.0 build
> esbuild --bundle src/index.ts --minify --loader:.png=file src/index.html --loader:.html=copy --outdir=dist


  dist\index.js           302b 
  dist\index.html         210b 
  dist\index.css           81b 
  dist\logo-55DNWN2R.png    0b 

Done in 9ms
Serving "dist" at http://127.0.0.1:8080
Ready for changes
Enter fullscreen mode Exit fullscreen mode

http://127.0.0.1:8080

The server watches your output files for changes. So every time you re-run the build command (for example, after editing a TypeScript or CSS file), live-server will automatically refresh the browser to show your latest updates. This live-reloading behavior speeds up your development workflow by keeping your browser in sync with your code—no need to manually refresh the page.

While esbuild doesn’t provide Hot Module Replacement (HMR) out of the box like some other bundlers (e.g. Webpack), this simple setup with live-server gives you a fast, zero-config way to preview changes in real-time and iterate quickly as you build your project.

11. Build the Project ⚒️

While the start script is great for development, the build script is essential when you’re ready to prepare your project for production. Running npm run build will trigger esbuild to bundle and optimize your project for deployment.

npm run build
Enter fullscreen mode Exit fullscreen mode

This command tells esbuild to execute the bundling process according to the configuration set in your esbuild.config.ts file (or esbuild options specified in the script). By default, esbuild will process all your assets—JavaScript, TypeScript, images, and CSS—and output them into the dist/ directory.

C:\Users\Robin\ts-esbuild-app>npm run build

> ts-esbuild-app@1.0.0 build
> esbuild --bundle src/index.ts --minify --loader:.png=file src/index.html --loader:.html=copy --outdir=dist


  dist\index.js           302b 
  dist\index.html         210b 
  dist\index.css           81b 
  dist\logo-55DNWN2R.png    0b 

Done in 9ms
Enter fullscreen mode Exit fullscreen mode

If you’ve configured esbuild to run in production mode (by using the --minify flag, which we did, or through the build configuration), esbuild will automatically apply a series of optimizations to the output files. These optimizations include:

  • Minification: esbuild will remove unnecessary whitespace, comments, and unused code from your JavaScript and TypeScript files, reducing their size significantly.

  • Tree-shaking: esbuild will eliminate any unused code that isn’t referenced anywhere in your project, further reducing the bundle size.

  • Asset optimization: While esbuild doesn’t handle image optimization by default, you can easily use plugins (like esbuild-plugin-image) to process image files and optimize them during the build.

Once the build process completes, you’ll find your bundled JavaScript in the dist/ directory (e.g., dist/index.js). This version of your application is optimized for performance and is ready for production deployment.

By using the build command, you ensure that your project is fully optimized and prepared to be served to users with the best possible performance.


Conclusion 📣

In this guide, we’ve explored how to create a lightweight, framework-free TypeScript project using esbuild. From setting up the project structure to configuring esbuild and enabling TypeScript compilation, you now have a streamlined, production-ready environment. Along the way, we’ve touched on key topics such as tree shaking, asset management, and optimizing both development and production builds for performance.

While frameworks like React or Angular come with pre-configured setups, using a custom TypeScript and esbuild configuration offers you complete control over your build process. This approach is especially beneficial for modular libraries, utilities, or smaller applications that don’t require the overhead of a full framework.

By harnessing esbuild’s fast bundling and optimization capabilities, you can reduce load times and enhance performance. These improvements make your project more efficient and scalable, enabling it to grow from small utilities to large-scale applications, all while keeping things lightweight.

Whether you’re new to TypeScript and esbuild or refining your existing workflow, this guide provides a solid foundation for building, bundling, and optimizing modern JavaScript applications. With a deep understanding of esbuild’s features, you’re now equipped to handle complex web projects with complete control over your application’s build process.


🙏 Thanks for reading to the end! If you have any questions, feel free to drop a comment below.

If you enjoyed this article, follow me on Medium and social media — I’d love to connect and follow you back:

Quadratic AI

Quadratic AI – The Spreadsheet with AI, Code, and Connections

  • AI-Powered Insights: Ask questions in plain English and get instant visualizations
  • Multi-Language Support: Seamlessly switch between Python, SQL, and JavaScript in one workspace
  • Zero Setup Required: Connect to databases or drag-and-drop files straight from your browser
  • Live Collaboration: Work together in real-time, no matter where your team is located
  • Beyond Formulas: Tackle complex analysis that traditional spreadsheets can't handle

Get started for free.

Watch The Demo 📊✨

Top comments (0)

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, valued within the supportive DEV Community. Coders of every background are welcome to join in and add to our collective wisdom.

A sincere "thank you" often brightens someone’s day. Share your gratitude in the comments below!

On DEV, the act of sharing knowledge eases our journey and fortifies our community ties. Found value in this? A quick thank you to the author can make a significant impact.

Okay