The JavaScript ecosystem is evolving at lightning speed. In 2025, full stack development continues to leverage Node.js as a stable and mature backend runtime. But new tools like Bun have emerged, promising extreme performance, faster bundling, and modern features for web applications.
In this article, we will explore how to build a full stack application using Node.js and Bun, covering architecture, tooling, backend and frontend integration, performance optimization, and production-ready best practices. The focus is on practical implementation with real-world considerations, making it highly relevant for senior developers.
Why Node.js + Bun in 2025?
Node.js has remained the cornerstone of backend JavaScript for years, with a rich ecosystem, stable tooling, and enterprise support. However, Bun is gaining attention for the following reasons:
Performance: Bun's runtime and bundler are built in Zig, offering faster startup times and native performance for server-side apps.
Bundling & Transpiling: Bun integrates bundling and transpiling in a single step, reducing build complexity.
Modern APIs: Bun provides native support for fetch, WebSocket, and other modern web standards without extra packages.
Full Stack Integration: With Bun, you can run server, API, and frontend code in a single environment, simplifying development.
By combining Node.js for stability and Bun for speed, developers get the best of both worlds: a mature ecosystem with bleeding-edge performance.
Step 1: Setting Up the Development Environment
Installing Node.js
Node.js is required for the existing ecosystem and npm package support:
# macOS/Linux
brew install node
# Windows (using nvm)
nvm install --lts
Verify installation:
node -v
npm -v
Installing Bun
Bun can be installed via a simple script:
curl -fsSL https://bun.sh/install | bash
Verify installation:
bun -v
Project Structure
A typical full stack app structure:
my-fullstack-app/
│
├─ backend/ # Node.js/Bun backend
│ ├─ controllers/
│ ├─ models/
│ ├─ routes/
│ ├─ app.js
│
├─ frontend/ # Angular / React / Vue frontend
│ ├─ src/
│ ├─ angular.json
│
├─ package.json
└─ bun.lockb
Keeping backend and frontend separated ensures modularity and easier CI/CD.
Step 2: Backend with Node.js + Bun
Node.js provides a stable environment for APIs, while Bun speeds up startup, native module loading, and bundling.
1. Creating the Backend Entry Point
// backend/app.js
import express from "express";
const app = express();
app.use(express.json());
app.get("/api/hello", (req, res) => {
res.json({ message: "Hello from Node + Bun backend!" });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Run with Node.js:
node backend/app.js
Run with Bun:
bun backend/app.js
2. Setting Up Routing and Controllers
Example: A simple REST API for tasks
// backend/routes/taskRoutes.js
import express from "express";
import { getTasks, addTask } from "../controllers/taskController.js";
const router = express.Router();
router.get("/", getTasks);
router.post("/", addTask);
export default router;
// backend/controllers/taskController.js
let tasks = [];
export const getTasks = (req, res) => res.json(tasks);
export const addTask = (req, res) => {
const { title } = req.body;
const newTask = { id: tasks.length + 1, title };
tasks.push(newTask);
res.status(201).json(newTask);
};
Integrate routes in app.js:
import taskRoutes from "./routes/taskRoutes.js";
app.use("/api/tasks", taskRoutes);
3. Database Integration
In 2025, Postgres, MySQL, MongoDB, and SQLite are commonly used. Using Prisma makes integration easier.
bun add prisma @prisma/client
npx prisma init
Example schema:
model Task {
id Int @id @default(autoincrement())
title String
done Boolean @default(false)
}
Generate client:
npx prisma generate
Backend API using Prisma:
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const getTasks = async (req, res) => {
const tasks = await prisma.task.findMany();
res.json(tasks);
};
Step 3: Frontend with Angular
Angular remains a popular choice in 2025 for enterprise-grade SPAs.
1. Initialize Angular Project
ng new frontend --routing --style=scss
cd frontend
ng serve
The Angular app will be served on http://localhost:4200.
2. Connecting Angular to Node + Bun Backend
Use Angular's HttpClientModule for API requests:
// frontend/src/app/services/task.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface Task {
id: number;
title: string;
done: boolean;
}
@Injectable({ providedIn: 'root' })
export class TaskService {
private apiUrl = 'http://localhost:3000/api/tasks';
constructor(private http: HttpClient) {}
getTasks(): Observable<Task[]> {
return this.http.get<Task[]>(this.apiUrl);
}
addTask(title: string): Observable<Task> {
return this.http.post<Task>(this.apiUrl, { title });
}
}
3. Displaying Tasks in Angular Component
// frontend/src/app/components/task/task.component.ts
import { Component, OnInit } from '@angular/core';
import { TaskService, Task } from '../../services/task.service';
@Component({
selector: 'app-task',
templateUrl: './task.component.html',
styleUrls: ['./task.component.scss']
})
export class TaskComponent implements OnInit {
tasks: Task[] = [];
newTask = '';
constructor(private taskService: TaskService) {}
ngOnInit() {
this.loadTasks();
}
loadTasks() {
this.taskService.getTasks().subscribe(tasks => this.tasks = tasks);
}
addTask() {
if (this.newTask.trim()) {
this.taskService.addTask(this.newTask).subscribe(task => {
this.tasks.push(task);
this.newTask = '';
});
}
}
}
<!-- frontend/src/app/components/task/task.component.html -->
<div class="task-wrapper">
<input [(ngModel)]="newTask" placeholder="Enter task" />
<button (click)="addTask()">Add Task</button>
<ul>
<li *ngFor="let task of tasks">{{ task.title }}</li>
</ul>
</div>
Step 4: Running Full Stack App
Start backend (Node.js or Bun):
bun backend/app.js
Start frontend:
ng serve
Open http://localhost:4200 and test adding tasks.
The Angular frontend calls the Node/Bun backend, which serves REST APIs.
Step 5: Production Best Practices in 2025
1. Use Environment Variables
const PORT = process.env.PORT || 3000;
const DB_URL = process.env.DATABASE_URL;
Never hardcode secrets.
2. Performance Optimization
Bun improves server startup and module load.
Enable HTTP/2 or Fastify if using Node.js backend.
Use caching (Redis, CDN).
Use Angular's lazy loading for modules and ng build --prod.
3. Security Best Practices
4. Logging and Monitoring
Bun has native performance metrics.
Use Winston or Pino in Node.js backend.
Monitor Angular frontend with tools like Sentry.
5. CI/CD Pipeline
Build frontend (ng build --prod) and backend (Bun bundle or Node.js dist).
Deploy on cloud platforms: AWS, GCP, Vercel, or Bun.sh server.
Use Bun's bundled artifacts for faster deployment.
Step 6: Advanced Patterns
1. Full Stack Realtime with WebSockets
Bun natively supports WebSocket APIs:
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', ws => {
ws.on('message', message => console.log(message.toString()));
});
Integrate Angular with RxJS for reactive updates.
2. Monorepo Architecture
Use Nx or Turborepo to manage frontend + backend in a single repository.
Shared libraries for models and interfaces.
Faster cross-team development.
3. Server-Side Rendering (Angular + Bun)
Use Angular Universal to pre-render pages.
Serve static pages via Bun for extreme speed.
Improves SEO and perceived performance.
4. Bun + Node Hybrid
Bun for dev, testing, and fast bundling.
Node.js for stable production environment with long-term LTS support.
This approach combines speed and stability.
Conclusion
In 2025, building full stack apps requires a careful balance between performance, developer experience, and ecosystem stability. Node.js provides maturity and wide library support, while Bun offers unmatched speed and modern APIs. Together, they enable:
Fast development and bundling
High-performance backend
Seamless frontend integration
Production-ready reliability
Senior developers can leverage Node.js + Bun to build scalable, maintainable, and future-proof applications. Whether you are building dashboards, SaaS platforms, or enterprise apps, this combination brings both speed and reliability to the full stack.