Frontend Engineer's Guide to Backend with Node.js, Bun & Deno

You've been writing React or Angular for years. You know components, state management, routing, and build tools inside out. But every time someone mentions "the backend," it feels like a different world — servers, databases, middleware, deployment pipelines.
Here's the truth: you already know more than you think. JavaScript is JavaScript. TypeScript is TypeScript. The mental models you've built for frontend development transfer directly. What you need isn't a new language — it's a new context.
This guide gives you a practical, no-fluff roadmap to go from frontend engineer to someone who can confidently build, deploy, and maintain backend services.
What You'll Learn
Learning Outcomes:
✅ What frontend skills transfer directly to backend development
✅ The key concepts you need to learn (and which ones you can skip)
✅ How to choose between Node.js, Bun, and Deno
✅ A step-by-step learning path from "hello world" to production API
✅ Common mistakes frontend engineers make on the backend
✅ How to build your first REST API from scratch
Part 1: What You Already Know (More Than You Think)
Before learning anything new, let's take inventory. As a frontend engineer, you already have:
JavaScript / TypeScript Mastery
This is the big one. The language is the same. Your knowledge of:
- Variables, functions, classes, modules
- Promises, async/await, error handling
- Array methods (
.map(),.filter(),.reduce()) - Destructuring, spread operators, template literals
- TypeScript types, interfaces, generics
All of this transfers 1:1 to the backend. You're not starting from zero.
Package Management
You already know npm (or yarn, pnpm). You've dealt with package.json, lockfiles, dependency conflicts, and semantic versioning. Backend Node.js projects use the exact same tooling.
Async Programming
This is where most backend beginners struggle — but you've been doing it for years. Every fetch() call, every state update after an API response, every useEffect with a cleanup function — that's async programming. On the backend, the same patterns apply to database queries, file reads, and HTTP requests.
JSON Everything
You already think in JSON. Request bodies, response payloads, configuration files — the data format doesn't change.
Build Tools and Dev Workflow
Webpack, Vite, esbuild, ESLint, Prettier, CI/CD pipelines — you've configured all of these. Backend projects use similar (often simpler) tooling.
Part 2: What's Actually New
Now let's be honest about what you don't know yet. These are the concepts that are genuinely new when moving to the backend.
1. HTTP from the Other Side
On the frontend, you send HTTP requests. On the backend, you receive them.
You've always been the client. Now you're the server. This means understanding:
- How to parse incoming requests (body, headers, query params, URL params)
- How to send responses (status codes, headers, JSON bodies)
- How routing works on the server (not React Router — actual URL → handler mapping)
| Frontend Perspective | Backend Perspective |
|---|---|
fetch('/api/users') | Receive request at /api/users |
Read response.json() | Write res.json({ users }) |
| Handle loading/error states | Handle validation/auth/errors |
| One user at a time | Thousands of concurrent requests |
2. Databases
This is the biggest knowledge gap for most frontend engineers. On the frontend, data comes from an API. On the backend, you are the one fetching it from a database.
You'll need to learn:
- SQL basics:
SELECT,INSERT,UPDATE,DELETE,JOIN - ORMs: Prisma, Drizzle, or TypeORM (they make SQL feel like TypeScript)
- Database design: Tables, relationships, indexes, migrations
- Connection management: Connection pools, connection strings
Good news: If you've ever used Prisma with Next.js, you already have a head start.
3. Authentication & Authorization
On the frontend, you store tokens and send them with requests. On the backend, you:
- Verify those tokens
- Hash passwords (never store plain text)
- Manage sessions or JWTs
- Decide who can access what (roles, permissions)
4. File System Access
Frontend code runs in a sandbox — no file access. Backend code can read and write files, create directories, stream large files, and watch for changes.
// This is impossible in the browser, normal on the backend
import { readFile, writeFile } from 'node:fs/promises';
const data = await readFile('./config.json', 'utf-8');
const config = JSON.parse(data);
config.lastUpdated = new Date().toISOString();
await writeFile('./config.json', JSON.stringify(config, null, 2));5. Environment & Security
On the frontend, everything is public — users can view your source code. On the backend:
- Secrets stay secret (API keys, database passwords)
- Environment variables (
.envfiles) hold configuration - Input validation is critical (never trust user input)
- You handle sensitive data (personal info, payments)
Part 3: Choosing Your Runtime
You have three main options for running JavaScript on the server. All three run the same language, but differ in philosophy and features.
Node.js — The Established Standard
- Created: 2009 by Ryan Dahl
- Engine: V8 (same as Chrome)
- Package manager: npm (world's largest registry)
- Ecosystem: Massive — Express, Fastify, NestJS, millions of packages
- When to choose: Production work, job market demand, maximum library support
# Install and run
node --version
node server.js
# Or with TypeScript
npx tsx server.tsBun — The Fast Newcomer
- Created: 2022 by Jarred Sumner
- Engine: JavaScriptCore (same as Safari)
- Package manager: Built-in (faster than npm)
- Ecosystem: Node.js compatible (most npm packages work)
- When to choose: Speed-critical projects, all-in-one tooling, new projects
# Install and run
bun --version
bun run server.ts # Native TypeScript support, no configDeno — The Secure Alternative
- Created: 2018 by Ryan Dahl (yes, same creator as Node.js)
- Engine: V8
- Package manager: URL imports +
deno.jsonimports map - Ecosystem: Growing, Node.js compatibility layer available
- When to choose: Security-first projects, clean module system, web standards focus
# Install and run
deno --version
deno run --allow-net --allow-read server.tsWhich Should You Pick?
| Factor | Node.js | Bun | Deno |
|---|---|---|---|
| Job market | 🟢 Dominant | 🟡 Growing | 🟡 Niche |
| Learning resources | 🟢 Abundant | 🟡 Moderate | 🟡 Moderate |
| Speed | 🟡 Good | 🟢 Fastest | 🟢 Fast |
| TypeScript | 🟡 Needs setup | 🟢 Native | 🟢 Native |
| npm compatibility | 🟢 Full | 🟢 Near-full | 🟡 Via compat layer |
| Security model | 🟡 Open by default | 🟡 Open by default | 🟢 Permissions-based |
My recommendation for frontend engineers: Start with Node.js. It has the most tutorials, the largest community, and the most job listings. Once you're comfortable, experiment with Bun or Deno — switching between them is trivial since the code is nearly identical.
Part 4: The Learning Roadmap
Here's a week-by-week plan to go from frontend engineer to backend-capable developer. You don't need to quit your job — 1-2 hours per day is enough.
Week 1-2: Node.js Fundamentals
Goal: Understand how Node.js works and build a basic HTTP server.
// server.js — Your first backend server
import { createServer } from 'node:http';
const server = createServer((req, res) => {
if (req.url === '/api/hello' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Hello from the backend!' }));
return;
}
res.writeHead(404);
res.end('Not found');
});
server.listen(3001, () => {
console.log('Server running on http://localhost:3001');
});What to study:
- Node.js built-in modules:
http,fs,path,crypto - The event loop (you already know it from browser JS — same concept, different APIs)
- CommonJS vs ESM (use ESM — you already know it from frontend)
- Reading environment variables with
process.env
Week 3-4: Pick a Framework
Raw http.createServer() is educational but impractical. Pick a framework:
Express.js (recommended for learning):
import express from 'express';
const app = express();
app.use(express.json());
const users = [];
app.get('/api/users', (req, res) => {
res.json(users);
});
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
const user = { id: Date.now(), name, email };
users.push(user);
res.status(201).json(user);
});
app.listen(3001, () => {
console.log('API running on http://localhost:3001');
});Other frameworks to know about:
| Framework | Style | Best For |
|---|---|---|
| Express | Minimal, middleware-based | Learning, small APIs |
| Fastify | Performance-focused | High-throughput APIs |
| NestJS | Full framework, Angular-inspired | Enterprise apps (familiar if you know Angular!) |
| Hono | Ultra-lightweight, multi-runtime | Edge functions, Bun/Deno/Cloudflare |
If you're an Angular developer, NestJS will feel like home — it uses decorators, dependency injection, modules, and a similar project structure. The transition is remarkably smooth.
Week 5-6: Database Integration
Time to replace your in-memory arrays with a real database.
Recommended path: PostgreSQL + Prisma ORM
npm install prisma @prisma/client
npx prisma initDefine your schema:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
content String
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}Use it in your API:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// GET /api/users
app.get('/api/users', async (req, res) => {
const users = await prisma.user.findMany({
include: { posts: true },
});
res.json(users);
});
// POST /api/users
app.post('/api/users', async (req, res) => {
try {
const user = await prisma.user.create({
data: { name: req.body.name, email: req.body.email },
});
res.status(201).json(user);
} catch (error) {
if (error.code === 'P2002') {
return res.status(409).json({ error: 'Email already exists' });
}
res.status(500).json({ error: 'Internal server error' });
}
});This should feel familiar — Prisma's API reads like TypeScript, not SQL. You get full autocompletion and type safety.
Week 7-8: Authentication & Middleware
Add JWT-based authentication to protect your routes:
import jwt from 'jsonwebtoken';
// Middleware — think of it like a React higher-order component for routes
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next(); // Pass to the next handler
} catch {
res.status(401).json({ error: 'Invalid token' });
}
}
// Protected route
app.get('/api/profile', authenticate, async (req, res) => {
const user = await prisma.user.findUnique({
where: { id: req.user.id },
});
res.json(user);
});Key concept for frontend devs: Middleware is like a chain of functions that process a request before it reaches your route handler. Think of it as a pipeline — similar to RxJS pipes in Angular or middleware in Redux.
Week 9-10: Testing & Error Handling
Write tests for your API. If you've used Jest or Vitest on the frontend, the testing tools are identical:
// users.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import { app } from './app';
describe('Users API', () => {
it('should create a new user', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Jane', email: 'jane@example.com' });
expect(response.status).toBe(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe('Jane');
});
it('should return 400 for missing fields', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Jane' });
expect(response.status).toBe(400);
});
});Week 11-12: Deployment
Deploy your API to the real world. Options:
| Platform | Difficulty | Cost | Best For |
|---|---|---|---|
| Railway | Easy | Free tier | Quick prototypes |
| Render | Easy | Free tier | Side projects |
| Fly.io | Medium | Free tier | Global distribution |
| AWS/GCP/Azure | Hard | Pay-as-you-go | Production at scale |
| Docker + VPS | Medium | $5-10/mo | Full control |
Minimum deployment checklist:
✅ Environment variables configured (no secrets in code)
✅ Database connection string set
✅ CORS configured for your frontend domain
✅ Error handling doesn't leak stack traces
✅ Health check endpoint (GET /health) working
✅ Logging set up for debugging production issues
Part 5: Common Mistakes Frontend Engineers Make
Mistake 1: Treating the Server Like the Browser
// ❌ DON'T: Global mutable state (works in browser, disaster on server)
let currentUser = null;
app.post('/api/login', (req, res) => {
currentUser = req.body.user; // Shared across ALL requests!
res.json({ success: true });
});
// ✅ DO: Each request is independent
app.post('/api/login', (req, res) => {
const user = req.body.user; // Scoped to this request
const token = generateToken(user);
res.json({ token });
});On the frontend, you have one user. On the backend, you have thousands of concurrent users sharing the same process. Never store per-request data in module-level variables.
Mistake 2: Not Handling Errors Properly
// ❌ DON'T: Unhandled errors crash the server
app.get('/api/users/:id', async (req, res) => {
const user = await prisma.user.findUnique({ where: { id: +req.params.id } });
res.json(user); // Crashes if user is null or id is NaN
});
// ✅ DO: Handle every possible failure
app.get('/api/users/:id', async (req, res) => {
try {
const id = parseInt(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ error: 'Invalid user ID' });
}
const user = await prisma.user.findUnique({ where: { id } });
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
console.error('Failed to fetch user:', error);
res.status(500).json({ error: 'Internal server error' });
}
});On the frontend, an error shows a blank screen. On the backend, an unhandled error crashes the entire server for all users.
Mistake 3: Blocking the Event Loop
// ❌ DON'T: Synchronous file read blocks ALL requests
import { readFileSync } from 'node:fs';
app.get('/api/report', (req, res) => {
const data = readFileSync('./large-report.csv', 'utf-8'); // Blocks!
res.json(processReport(data));
});
// ✅ DO: Use async operations
import { readFile } from 'node:fs/promises';
app.get('/api/report', async (req, res) => {
const data = await readFile('./large-report.csv', 'utf-8'); // Non-blocking
res.json(processReport(data));
});Mistake 4: Forgetting Input Validation
On the frontend, you validate for UX. On the backend, you validate for security.
// ❌ DON'T: Trust user input
app.post('/api/users', async (req, res) => {
const user = await prisma.user.create({ data: req.body }); // Accepts anything!
res.json(user);
});
// ✅ DO: Validate everything
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
});
app.post('/api/users', async (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
const user = await prisma.user.create({ data: result.data });
res.status(201).json(user);
});Bonus: You can share Zod schemas between frontend and backend — validate once, use everywhere.
Mistake 5: Not Understanding Concurrency
// ❌ DON'T: Think sequentially when requests overlap
let counter = 0;
app.post('/api/increment', async (req, res) => {
counter++; // Race condition with concurrent requests!
res.json({ counter });
});
// ✅ DO: Use atomic database operations
app.post('/api/increment', async (req, res) => {
const result = await prisma.counter.update({
where: { id: 1 },
data: { value: { increment: 1 } },
});
res.json({ counter: result.value });
});Part 6: Your First Full Project
Build a bookmark API — a backend service that lets users save and organize links. This project covers every concept from the roadmap.
Features:
- User registration and login (JWT auth)
- CRUD operations for bookmarks
- Tag-based organization
- Search functionality
- Input validation with Zod
Tech Stack:
- Runtime: Node.js
- Framework: Express.js
- Database: PostgreSQL + Prisma
- Auth: JWT + bcrypt
- Validation: Zod
- Testing: Vitest + Supertest
Project Structure:
bookmark-api/
├── src/
│ ├── routes/
│ │ ├── auth.ts # Login, register
│ │ ├── bookmarks.ts # CRUD for bookmarks
│ │ └── tags.ts # Tag management
│ ├── middleware/
│ │ ├── auth.ts # JWT verification
│ │ ├── validate.ts # Zod validation
│ │ └── errorHandler.ts # Global error handler
│ ├── schemas/
│ │ └── bookmark.ts # Zod schemas
│ ├── app.ts # Express app setup
│ └── server.ts # Server entry point
├── prisma/
│ └── schema.prisma # Database schema
├── tests/
│ └── bookmarks.test.ts # API tests
├── .env # Environment variables
├── package.json
└── tsconfig.jsonThis structure will feel very familiar if you've worked with Angular or a well-organized React project. The pattern of separating concerns into routes, middleware, and schemas maps directly to how you'd organize components, guards, and services in a frontend app.
Key Takeaways
✅ You're not starting from zero — JavaScript, TypeScript, async patterns, npm, and JSON knowledge all transfer directly
✅ The main new concepts are: HTTP from the server side, databases, authentication, and file system access
✅ Start with Node.js + Express — the largest ecosystem and most learning resources
✅ NestJS feels like Angular — if you're an Angular dev, the transition to NestJS is remarkably smooth
✅ Use Prisma for databases — it makes SQL feel like TypeScript with full autocompletion
✅ Every request is independent — don't use global mutable state on the server
✅ Validate all input — frontend validation is for UX, backend validation is for security
✅ Build a real project — theory only gets you so far, build a complete API end-to-end
✅ The gap is smaller than you think — most frontend engineers can build production APIs within 2-3 months
Further Reading
If you want to dive deeper into the topics covered in this post:
- JavaScript: Browser vs Server — What's Actually Different? — understand the fundamental differences between the two environments
- Getting Started with Express.js — build your first Express.js server step by step
- Express.js Middleware Deep Dive — master middleware patterns for production APIs
What's Next?
You don't need permission to be a "backend engineer." You're a JavaScript developer — the backend is just a different room in the same house.
Pick one thing from this roadmap, spend an hour on it today, and keep going tomorrow. In a few weeks, you'll wonder why you waited so long.
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.