Back to blog

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

nodejsbackendjavascripttypescriptcareer
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 PerspectiveBackend Perspective
fetch('/api/users')Receive request at /api/users
Read response.json()Write res.json({ users })
Handle loading/error statesHandle validation/auth/errors
One user at a timeThousands 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 (.env files) 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.ts

Bun — 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 config

Deno — The Secure Alternative

  • Created: 2018 by Ryan Dahl (yes, same creator as Node.js)
  • Engine: V8
  • Package manager: URL imports + deno.json imports 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.ts

Which Should You Pick?

FactorNode.jsBunDeno
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:

FrameworkStyleBest For
ExpressMinimal, middleware-basedLearning, small APIs
FastifyPerformance-focusedHigh-throughput APIs
NestJSFull framework, Angular-inspiredEnterprise apps (familiar if you know Angular!)
HonoUltra-lightweight, multi-runtimeEdge 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 init

Define 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:

PlatformDifficultyCostBest For
RailwayEasyFree tierQuick prototypes
RenderEasyFree tierSide projects
Fly.ioMediumFree tierGlobal distribution
AWS/GCP/AzureHardPay-as-you-goProduction at scale
Docker + VPSMedium$5-10/moFull 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.json

This 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:


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.