DEV Community

Cover image for Node.js Backend for Frontend Developers: Express, REST APIs, Auth, and Deployment (Step-by-Step)
Umesh Malik
Umesh Malik

Posted on • Edited on • Originally published at umesh-malik.com

Node.js Backend for Frontend Developers: Express, REST APIs, Auth, and Deployment (Step-by-Step)

As a frontend engineer who has built backend services at BYJU'S and for personal projects, I know the mental shift from frontend to backend isn't trivial. Here's what you need to know to build your first production-quality Node.js backend. If this mental-model shift lands, the same lens runs through my TypeScript utility types guide and this deep dive on cutting Node.js memory in half. See also Frontend Testing Strategies That Actually Work in 2026.

The Mindset Shift

Frontend and backend engineering have different concerns:

Frontend Backend
User experience Data integrity
Render performance Throughput and latency
Client state Database state
Browser APIs OS and network APIs
Graceful degradation Error handling and retries

The biggest adjustment: on the backend, data is the product. A UI bug is annoying; a data corruption bug can be catastrophic.

Project Setup with TypeScript

mkdir my-api && cd my-api
pnpm init
pnpm add express cors helmet
pnpm add -D typescript @types/express @types/node @types/cors tsx
Enter fullscreen mode Exit fullscreen mode
// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}
Enter fullscreen mode Exit fullscreen mode

Basic Express Server

// src/index.ts
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';

const app = express();
const PORT = process.env.PORT || 3001;

// Middleware
app.use(helmet());           // Security headers
app.use(cors());             // CORS for frontend
app.use(express.json());     // Parse JSON bodies

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Run with: pnpm tsx watch src/index.ts

Building REST Endpoints

Structure your routes by resource:

// src/routes/users.ts
import { Router } from 'express';

const router = Router();

interface User {
  id: string;
  name: string;
  email: string;
}

// In-memory store (replace with database)
const users: User[] = [];

// GET /api/users
router.get('/', (req, res) => {
  res.json(users);
});

// GET /api/users/:id
router.get('/:id', (req, res) => {
  const user = users.find((u) => u.id === req.params.id);
  if (!user) return res.status(404).json({ error: 'User not found' });
  res.json(user);
});

// POST /api/users
router.post('/', (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email are required' });
  }

  const user: User = {
    id: crypto.randomUUID(),
    name,
    email,
  };

  users.push(user);
  res.status(201).json(user);
});

// DELETE /api/users/:id
router.delete('/:id', (req, res) => {
  const index = users.findIndex((u) => u.id === req.params.id);
  if (index === -1) return res.status(404).json({ error: 'User not found' });

  users.splice(index, 1);
  res.status(204).send();
});

export default router;
Enter fullscreen mode Exit fullscreen mode

Register routes in your main file:

// src/index.ts
import userRoutes from './routes/users';

app.use('/api/users', userRoutes);
Enter fullscreen mode Exit fullscreen mode

Middleware: The Backend Equivalent of HOCs

Middleware in Express is like higher-order components in React — they wrap your handlers with additional behavior.

Error Handling Middleware

// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';

class AppError extends Error {
  constructor(
    public statusCode: number,
    message: string
  ) {
    super(message);
  }
}

function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({ error: err.message });
  }

  console.error('Unhandled error:', err);
  res.status(500).json({ error: 'Internal server error' });
}

export { AppError, errorHandler };
Enter fullscreen mode Exit fullscreen mode

Request Logging Middleware

// src/middleware/logger.ts
import { Request, Response, NextFunction } from 'express';

function logger(req: Request, res: Response, next: NextFunction) {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`);
  });

  next();
}

export { logger };
Enter fullscreen mode Exit fullscreen mode

Database Basics with MongoDB

// src/db.ts
import { MongoClient, Db } from 'mongodb';

let db: Db;

async function connectDB() {
  const client = new MongoClient(process.env.MONGODB_URI || 'mongodb://localhost:27017');
  await client.connect();
  db = client.db('myapp');
  console.log('Connected to MongoDB');
}

function getDB(): Db {
  if (!db) throw new Error('Database not connected');
  return db;
}

export { connectDB, getDB };
Enter fullscreen mode Exit fullscreen mode

Update your user route to use the database:

// GET /api/users
router.get('/', async (req, res) => {
  const users = await getDB().collection('users').find().toArray();
  res.json(users);
});
Enter fullscreen mode Exit fullscreen mode

Authentication: JWT Basics

// src/middleware/auth.ts
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';

const JWT_SECRET = process.env.JWT_SECRET || 'change-this-in-production';

interface AuthRequest extends Request {
  userId?: string;
}

function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  try {
    const decoded = jwt.verify(token, JWT_SECRET) as { userId: string };
    req.userId = decoded.userId;
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
}

function generateToken(userId: string): string {
  return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '7d' });
}

export { authenticate, generateToken, AuthRequest };
Enter fullscreen mode Exit fullscreen mode

Environment Variables

// src/config.ts
import { z } from 'zod';

const envSchema = z.object({
  PORT: z.string().default('3001'),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  MONGODB_URI: z.string().url(),
  JWT_SECRET: z.string().min(32),
});

export const config = envSchema.parse(process.env);
Enter fullscreen mode Exit fullscreen mode

Using Zod for environment validation catches missing variables at startup instead of at runtime when a request hits the missing value.

Error Handling Patterns

// Wrap async route handlers to catch promise rejections
function asyncHandler(fn: Function) {
  return (req: Request, res: Response, next: NextFunction) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

// Usage
router.get('/:id', asyncHandler(async (req, res) => {
  const user = await getDB().collection('users').findOne({ _id: req.params.id });
  if (!user) throw new AppError(404, 'User not found');
  res.json(user);
}));
Enter fullscreen mode Exit fullscreen mode

Project Structure

src/
├── index.ts          # Entry point, server setup
├── config.ts         # Environment variables
├── db.ts             # Database connection
├── routes/
│   ├── users.ts      # User endpoints
│   └── auth.ts       # Auth endpoints
├── middleware/
│   ├── auth.ts       # JWT authentication
│   ├── logger.ts     # Request logging
│   └── errorHandler.ts
└── types/
    └── index.ts      # Shared TypeScript types
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • Start with Express + TypeScript — it's the most transferable backend skill
  • Think about data integrity first, then performance
  • Middleware is your primary tool for cross-cutting concerns (auth, logging, errors)
  • Validate everything at the boundary (request body, env vars, query params)
  • Use async/await with proper error handling — unhandled rejections crash Node processes
  • Keep your project structure flat and organized by feature, not by type
  • Learn SQL basics even if you start with MongoDB — most companies use relational databases

Originally published at umesh-malik.com

Keep reading on umesh-malik.com:

Top comments (0)