Node.js Backend Development Complete Guide
Introduction
Node.js has become the go-to platform for building scalable backend applications. This comprehensive guide covers Express.js fundamentals, middleware, authentication, database integration, error handling, and production best practices.
1. Project Setup
Initialize Node.js Project
# Create project directory
mkdir my-backend-api
cd my-backend-api
# Initialize npm
npm init -y
# Install core dependencies
npm install express dotenv cors helmet
npm install --save-dev nodemon typescript @types/node @types/express
# Create TypeScript config
npx tsc --init
Project Structure
my-backend-api/
├── src/
│ ├── config/
│ │ ├── database.ts
│ │ └── env.ts
│ ├── controllers/
│ │ ├── auth.controller.ts
│ │ └── user.controller.ts
│ ├── middleware/
│ │ ├── auth.middleware.ts
│ │ ├── error.middleware.ts
│ │ └── validation.middleware.ts
│ ├── models/
│ │ └── user.model.ts
│ ├── routes/
│ │ ├── auth.routes.ts
│ │ └── user.routes.ts
│ ├── services/
│ │ └── user.service.ts
│ ├── utils/
│ │ ├── logger.ts
│ │ └── asyncHandler.ts
│ ├── types/
│ │ └── express.d.ts
│ └── server.ts
├── .env
├── .env.example
├── .gitignore
├── package.json
└── tsconfig.json
2. Express.js Server Setup
Basic Server Configuration
// src/server.ts
import express, { Application, Request, Response } from 'express';
import cors from 'cors';
import helmet from 'helmet';
import dotenv from 'dotenv';
import { errorHandler } from './middleware/error.middleware';
import authRoutes from './routes/auth.routes';
import userRoutes from './routes/user.routes';
dotenv.config();
const app: Application = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(helmet()); // Security headers
app.use(cors({
origin: process.env.FRONTEND_URL,
credentials: true
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Request logging
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
next();
});
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes);
// Health check
app.get('/health', (req: Request, res: Response) => {
res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() });
});
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
// Error handling middleware
app.use(errorHandler);
// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
3. Database Integration
MongoDB with Mongoose
// src/config/database.ts
import mongoose from 'mongoose';
export const connectDB = async (): Promise => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI!);
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error('MongoDB connection error:', error);
process.exit(1);
}
};
// src/models/user.model.ts
import mongoose, { Document, Schema } from 'mongoose';
import bcrypt from 'bcryptjs';
export interface IUser extends Document {
email: string;
password: string;
name: string;
role: 'user' | 'admin';
createdAt: Date;
comparePassword(candidatePassword: string): Promise;
}
const userSchema = new Schema({
email: {
type: String,
required: true,
unique: true,
lowercase: true,
trim: true
},
password: {
type: String,
required: true,
minlength: 8
},
name: {
type: String,
required: true
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
createdAt: {
type: Date,
default: Date.now
}
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Compare password method
userSchema.methods.comparePassword = async function(candidatePassword: string): Promise {
return await bcrypt.compare(candidatePassword, this.password);
};
export default mongoose.model('User', userSchema);
PostgreSQL with Prisma
// Install Prisma
npm install @prisma/client
npm install -D prisma
// Initialize Prisma
npx prisma init
// schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String
name String
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
createdAt DateTime @default(now())
}
enum Role {
USER
ADMIN
}
// Generate Prisma client
npx prisma generate
// src/config/prisma.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
log: ['query', 'error', 'warn'],
});
export default prisma;
4. Authentication & Authorization
JWT Authentication
// src/controllers/auth.controller.ts
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import User from '../models/user.model';
export const register = async (req: Request, res: Response) => {
try {
const { email, password, name } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'User already exists' });
}
// Create user
const user = await User.create({ email, password, name });
// Generate token
const token = jwt.sign(
{ userId: user._id, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '7d' }
);
res.status(201).json({
message: 'User created successfully',
token,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role
}
});
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
};
export const login = async (req: Request, res: Response) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const isMatch = await user.comparePassword(password);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate token
const token = jwt.sign(
{ userId: user._id, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '7d' }
);
res.json({
token,
user: {
id: user._id,
email: user.email,
name: user.name,
role: user.role
}
});
} catch (error) {
res.status(500).json({ error: 'Server error' });
}
};
Authentication Middleware
// src/middleware/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import User from '../models/user.model';
interface JwtPayload {
userId: string;
role: string;
}
declare global {
namespace Express {
interface Request {
user?: any;
}
}
}
export const authenticate = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
// Get token from header
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
// Find user
const user = await User.findById(decoded.userId).select('-password');
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
// Attach user to request
req.user = user;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};
export const authorize = (...roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Access denied' });
}
next();
};
};
5. Error Handling
Custom Error Classes
// src/utils/errors.ts
export class AppError extends Error {
statusCode: number;
isOperational: boolean;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
export class ValidationError extends AppError {
constructor(message: string) {
super(message, 400);
}
}
export class UnauthorizedError extends AppError {
constructor(message: string = 'Unauthorized') {
super(message, 401);
}
}
export class NotFoundError extends AppError {
constructor(message: string = 'Resource not found') {
super(message, 404);
}
}
Global Error Handler
// src/middleware/error.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../utils/errors';
export const errorHandler = (
err: Error,
req: Request,
res: Response,
next: NextFunction
) => {
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
}
// Mongoose validation error
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'Validation error',
details: err.message
});
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Invalid token' });
}
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
// Default error
console.error('Unhandled error:', err);
res.status(500).json({
error: 'Internal server error',
...(process.env.NODE_ENV === 'development' && {
message: err.message,
stack: err.stack
})
});
};
// Async handler wrapper
export const asyncHandler = (fn: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
6. Validation
Request Validation with Joi
// Install Joi
npm install joi
// src/validation/auth.validation.ts
import Joi from 'joi';
export const registerSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
name: Joi.string().min(2).max(50).required()
});
export const loginSchema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().required()
});
// src/middleware/validation.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { Schema } from 'joi';
export const validate = (schema: Schema) => {
return (req: Request, res: Response, next: NextFunction) => {
const { error } = schema.validate(req.body, { abortEarly: false });
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({
error: 'Validation failed',
details: errors
});
}
next();
};
};
7. Rate Limiting
// Install rate limiter
npm install express-rate-limit
// src/middleware/rateLimit.middleware.ts
import rateLimit from 'express-rate-limit';
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 login attempts per 15 minutes
skipSuccessfulRequests: true,
message: 'Too many login attempts, please try again later'
});
// Apply to routes
import { apiLimiter, authLimiter } from './middleware/rateLimit.middleware';
app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);
8. Logging
// Install Winston
npm install winston
// src/utils/logger.ts
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
export default logger;
9. Environment Configuration
// .env.example
NODE_ENV=development
PORT=3000
# Database
MONGODB_URI=mongodb://localhost:27017/myapp
# or
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
# JWT
JWT_SECRET=your_super_secret_jwt_key_change_in_production
JWT_EXPIRE=7d
# CORS
FRONTEND_URL=http://localhost:3000
# API Keys
SENDGRID_API_KEY=your_sendgrid_key
AWS_ACCESS_KEY_ID=your_aws_key
# Logging
LOG_LEVEL=debug
10. Best Practices
✓ Node.js Backend Checklist:
- Use TypeScript for type safety
- Implement proper error handling
- Validate all user inputs
- Use environment variables for configuration
- Implement authentication & authorization
- Add rate limiting to prevent abuse
- Use helmet.js for security headers
- Implement logging (Winston/Morgan)
- Handle async errors properly
- Use connection pooling for databases
- Implement CORS correctly
- Add health check endpoints
- Use compression middleware
- Implement graceful shutdown
- Monitor performance (APM tools)
Conclusion
Building production-ready Node.js backends requires careful attention to security, error handling, validation, and scalability. By following these patterns and best practices, you can create robust APIs that handle real-world traffic and edge cases effectively.
💡 Pro Tip: Always use a process manager like PM2 in production, implement graceful shutdown handlers, and monitor your application with tools like New Relic or Datadog to catch issues before they impact users.