The JavaScript ecosystem is evolving at a breathtaking pace, and 2025 has cemented a new, powerful trifecta for building modern, high-performance full-stack applications: Bun.js as the runtime, React for the user interface, and Drizzle ORM for database interaction. This combination leverages the raw speed of Bun, the declarative power of React, and the type-safe, SQL-first philosophy of Drizzle to create a developer experience that is both incredibly fast and remarkably robust. In this comprehensive guide, we’ll walk through building a simple but functional full-stack app from the ground up, demonstrating best practices and the unique advantages of this modern stack.
Table of Contents
Why This Stack? The 2025 Advantage
Before diving into the code, it’s essential to understand why this specific combination is so compelling in the current landscape.
Bun.js: The All-in-One Runtime Revolution Bun is not just a faster Node.js replacement; it’s a complete JavaScript/TypeScript runtime and tooling suite built from the ground up in Zig. Its key advantages include:
- Blazing-Fast Execution: Bun’s JavaScriptCore-based engine compiles and runs code significantly faster than Node.js, leading to quicker server startups and a snappier development experience.
- Native Bundler and Test Runner: Bun comes with its own built-in bundler (
bun build) and test runner (bun test), eliminating the need for complex Webpack or Jest configurations for many projects. - First-Class TypeScript Support: TypeScript files are compiled on the fly with zero configuration, streamlining the development workflow.
React: The Declarative UI Standard React remains the dominant library for building user interfaces. Its component-based architecture, virtual DOM, and rich ecosystem make it the ideal choice for creating dynamic and responsive frontends. In 2025, its integration with modern runtimes like Bun is seamless.
Drizzle ORM: Type-Safe, SQL-Centric Data Access Drizzle ORM has emerged as a favorite among developers who want the benefits of an ORM without sacrificing control over their SQL. It provides:
- SQL-Like Syntax: Its query builder closely mirrors raw SQL, making it intuitive for anyone familiar with databases.
- End-to-End Type Safety: From your database schema definition to your query results, Drizzle provides robust TypeScript types, catching errors at compile time.
- Database Agnostic: It supports a wide range of databases, including SQLite, PostgreSQL, MySQL, and more.
Together, these tools create a cohesive, high-performance, and type-safe full-stack environment that is a joy to develop in.
Prerequisites and Project Setup
To follow this guide, you’ll need the following installed on your machine:
- Bun.js (v1.0 or later). You can install it from https://bun.sh .
- A code editor of your choice (VS Code is highly recommended).
- A terminal.
We’ll be using SQLite for our database for its simplicity, but the Drizzle setup can be easily adapted to PostgreSQL or MySQL.
Step 1: Initialize the Project Create a new project directory and initialize it with Bun.
mkdir my-bun-fullstack-app
cd my-bun-fullstack-app
bun init
When prompted, you can accept the defaults or customize them. This creates a package.json file.
Step 2: Install Core Dependencies Install the essential libraries for both the frontend and backend.
# Core dependencies
bun add react react-dom drizzle-orm
# For the backend server
bun add -d typescript @types/react @types/react-dom
# For SQLite (use drizzle-orm/pg-core for PostgreSQL, etc.)
bun add better-sqlite3
bun add -d drizzle-kit
We install drizzle-orm as a main dependency and its peer libraries (like better-sqlite3 for our database driver and drizzle-kit for schema management) as developer dependencies.
Building the Backend with Bun.js and Drizzle ORM
Our backend will be a simple REST API built with Bun’s native HTTP server.
Step 1: Define the Database Schema Create a src directory and inside it, a db folder. In src/db/schema.ts, define your database tables using Drizzle’s schema builder.
// src/db/schema.ts
import { integer,sqliteTable,text } from'drizzle-orm/sqlite-core';
exportconstposts=sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$default(() => newDate()),
});
This defines a simple posts table with an auto-incrementing ID, a title, content, and a timestamp.
Step 2: Initialize the Database Connection In src/db/index.ts, set up the connection to your SQLite database using Drizzle.
// src/db/index.ts
import { drizzle } from'drizzle-orm/better-sqlite3';
import { Database } from'better-sqlite3';
import*asschemafrom'./schema';
constsqlite=newDatabase('sqlite.db');
exportconstdb=drizzle(sqlite, { schema });
This creates a connection to a local sqlite.db file and passes the schema to Drizzle for type safety.
Step 3: Create a Simple API Server Now, create the main server file at src/server.ts.
// src/server.ts
import { db } from'./db';
import { posts } from'./db/schema';
import { Hono } from'hono';// We'll use Hono, a lightweight web framework that works brilliantly with Bun
import { cors } from'hono/cors';
constapp=newHono();
// Enable CORS for our frontend
app.use('*',cors());
// GET all posts
app.get('/api/posts',async (c) => {
constallPosts=awaitdb.select().from(posts);
returnc.json(allPosts);
});
// POST a new post
app.post('/api/posts',async (c) => {
const { title,content } =awaitc.req.json();
constnewPost=awaitdb.insert(posts).values({ title,content }).returning();
returnc.json(newPost[0],201);
});
// Serve the React app on the root route (for production)
app.get('/', (c) => {
returnc.html('<div id="root"></div>');
});
exportdefaultapp;
We’re using Hono, a minimalist and ultra-fast web framework that is a perfect match for Bun’s speed. It provides a clean and simple API for defining routes. The server defines two endpoints: one to fetch all posts and another to create a new one.
Step 4: Set Up Drizzle Kit for Migrations To manage our database schema over time, we’ll use Drizzle Kit. Create a drizzle.config.ts file in your project root.
// drizzle.config.ts
importtype { Config } from'drizzle-kit';
exportdefault {
schema: './src/db/schema.ts',
out: './drizzle',
driver: 'sqlite',
dbCredentials: {
url: 'sqlite.db',
},
} satisfiesConfig;
Now, you can generate your initial migration with:
bun run drizzle-kit generate
This will create a drizzle folder with a SQL migration file. To apply this migration and create your tables, you need to write a small script. Create src/db/migrate.ts:
// src/db/migrate.ts
import { drizzle } from'drizzle-orm/better-sqlite3';
import { migrate } from'drizzle-orm/better-sqlite3/migrator';
import { Database } from'better-sqlite3';
constsqlite=newDatabase('sqlite.db');
constdb=drizzle(sqlite);
migrate(db, { migrationsFolder: './drizzle' });
console.log('Migration complete!');
You can run this script with bun run src/db/migrate.ts to set up your database.
Building the Frontend with React
We’ll create a simple React frontend that can list and create new posts. We’ll use Bun’s built-in bundler to serve this frontend.
Step 1: Create the React Application Structure Inside your src directory, create a client folder for the frontend code. Create src/client/main.tsx as the entry point.
// src/client/main.tsx
importReactfrom'react';
import { createRoot } from'react-dom/client';
importAppfrom'./App';
constcontainer=document.getElementById('root');
if (container) {
constroot=createRoot(container);
root.render(<App />);
}
Now, create the main App component at src/client/App.tsx.
// src/client/App.tsx
importReact, { useState,useEffect } from'react';
interfacePost {
id: number;
title: string;
content: string;
createdAt: string;
}
constApp: React.FC= () => {
const [posts,setPosts] =useState<Post[]>([]);
const [title,setTitle] =useState('');
const [content,setContent] =useState('');
// Fetch posts on component mount
useEffect(() => {
constfetchPosts=async () => {
constresponse=awaitfetch('/api/posts');
constdata=awaitresponse.json();
setPosts(data);
};
fetchPosts();
}, []);
consthandleSubmit=async (e: React.FormEvent) => {
e.preventDefault();
constresponse=awaitfetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title,content }),
});
constnewPost=awaitresponse.json();
setPosts([...posts,newPost]);
setTitle('');
setContent('');
};
return (
<div style={{ padding: '20px',fontFamily: 'sans-serif' }}>
<h1>My Bun Full-Stack App</h1>
<form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
required
style={{ display: 'block',marginBottom: '10px',padding: '8px' }}
/>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Content"
required
rows={4}
style={{ display: 'block',marginBottom: '10px',padding: '8px' }}
/>
<button type="submit"style={{ padding: '10px 15px' }}>Create Post</button>
</form>
<div>
{posts.map(post => (
<div key={post.id} style={{ border: '1px solid #ccc',padding: '15px',marginBottom: '10px' }}>
<h2>{post.title}</h2>
<p>{post.content}</p>
<small>{new Date(post.createdAt).toLocaleString()}</small>
</div>
))}
</div>
</div>
);
};
exportdefaultApp;
This component manages its state for the form inputs and the list of posts. It fetches the initial list of posts from the backend on mount and provides a form to create new ones.
Step 2: Create the Frontend Bundle We need to bundle our React code so the browser can understand it. Create a bun.build.js configuration file in your project root.
// bun.build.js
exportdefault {
entrypoints: ["./src/client/main.tsx"],
outdir: "./public",
format: "esm",
target: "browser",
minify: true,
};
This tells Bun to bundle our main.tsx file and output it to a public directory. Run the build command:
bun build --config bun.build.js
This will generate a public/main.js file.
Step 3: Serve the Full Application We need to modify our Hono server to serve the static assets from the public folder and the index.html that loads our bundle. First, create a simple public/index.html file.
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>My Bun App</title>
</head>
<body>
<divid="root"></div>
<scripttype="module"src="/main.js"></script>
</body>
</html>
Now, update your src/server.ts to serve static files.
// Add this to your imports in src/server.ts
import { serveStatic } from'hono/bun';
// ... your existing app setup ...
// Serve static files from the public directory
app.use('/main.js',serveStatic({ path: './public/main.js' }));
app.use('/',serveStatic({ path: './public/index.html' }));
Finally, create a start script in your package.json to run the server:
{
// ... other fields ...
"scripts": {
"dev":"bun run --watch src/server.ts"
}
}
The --watch flag will restart the server whenever you make a change to your backend code, providing a great development experience.
Running the Application
You’re now ready to run your full-stack application.
- Migrate the Database:
bun run src/db/migrate.ts - Build the Frontend:
bun build --config bun.build.js - Start the Development Server:
bun run dev
Navigate to http://localhost:3000 (or the port your Hono server is running on) in your browser. You should see your React app, be able to create new posts, and see them listed in real-time. The entire stack, from the database query to the React component update, is working in harmony.
Advantages and Future Considerations
This starter project showcases the raw power and simplicity of the Bun + React + Drizzle stack. You’ve built a type-safe, full-stack application with minimal configuration, leveraging the speed of Bun for both development and runtime.
For a production application, you would next consider:
- Adding Authentication: Integrating a library like Auth.js.
- Enhancing Validation: Using Zod for input validation on both the frontend and backend.
- State Management: For more complex apps, you might add a library like Zustand.
- Database Choice: Switching from SQLite to PostgreSQL for production by simply changing the Drizzle driver and configuration.
- Dockerization: Containerizing your app for easy deployment.
In 2025, this stack represents a significant leap forward in developer productivity and application performance. By embracing these modern tools, you’re not just building an application; you’re building it on the foundation of the JavaScript ecosystem’s future.