Complete Guide to Node.js and Express.js

A comprehensive reference for building modern web applications and APIs with Node.js and Express.js

 

Table of Contents

1. Introduction to Node.js and Express.js

Node.js is a JavaScript runtime built on Chrome’s V8 JavaScript engine that allows you to run JavaScript on the server side. Express.js is a minimal and flexible Node.js web application framework that provides a robust set of features for building web and mobile applications.

Node.js and Express.js allow you to:

  • Build scalable server-side applications using JavaScript
  • Create RESTful APIs and microservices
  • Handle real-time applications with WebSockets
  • Process file uploads and serve static content

2. Environment Setup

Installing Node.js

# Using nvm (recommended)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install --lts
nvm use --lts

# Verify installation
node --version
npm --version

Creating a New Project

# Create project directory
mkdir my-express-app
cd my-express-app

# Initialize package.json
npm init -y

# Install Express.js and dependencies
npm install express mongoose bcryptjs jsonwebtoken cors helmet
npm install --save-dev nodemon dotenv jest

Package.json Configuration

{
"name": "my-express-app",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest"
},
"dependencies": {
"express": "^4.18.2",
"mongoose": "^7.5.0",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"cors": "^2.8.5",
"helmet": "^7.0.0"
},
"devDependencies": {
"nodemon": "^3.0.1",
"dotenv": "^16.3.1",
"jest": "^29.6.4"
}
}

3. Express.js Fundamentals

Basic Server Setup

// server.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
require('dotenv').config();

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

// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Routes
app.get('/', (req, res) => {
res.json({
message: 'Welcome to Express.js API',
version: '1.0.0'
});
});

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

// Connect to MongoDB
mongoose.connect(process.env.DB_CONNECTION_STRING)
.then(() => console.log('MongoDB connected'))
.catch(err => console.error('MongoDB connection error:', err));

// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

module.exports = app;

Environment Variables

# .env
NODE_ENV=development
PORT=3000
DB_CONNECTION_STRING=mongodb://localhost:27017/myapp
JWT_SECRET=your_super_secret_key_here
JWT_EXPIRE=7d

4. Routing and Middleware

Express Router

// routes/users.js
const express = require('express');
const User = require('../models/User');
const router = express.Router();

// GET /users - Get all users
router.get('/', async (req, res) => {
try {
const { page = 1, limit = 10 } = req.query;
const users = await User.find()
.limit(limit * 1)
.skip((page - 1) * limit)
.select('-password');

res.json(users);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// POST /users - Create user
router.post('/', async (req, res) => {
try {
const user = new User(req.body);
await user.save();
res.status(201).json(user);
} catch (error) {
res.status(400).json({ error: error.message });
}
});

// GET /users/:id - Get single user
router.get('/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id).select('-password');
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

module.exports = router;

Authentication Middleware

// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');

const auth = async (req, res, next) => {
try {
const token = req.header('Authorization')?.replace('Bearer ', '');

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

const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId).select('-password');

req.user = user;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
};

module.exports = auth;

5. Database Integration

User Model

// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 6
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
}
}, {
timestamps: true
});

// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});

// Compare password
userSchema.methods.comparePassword = async function(password) {
return await bcrypt.compare(password, this.password);
};

// Generate JWT
userSchema.methods.generateToken = function() {
return jwt.sign(
{ userId: this._id },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRE }
);
};

module.exports = mongoose.model('User', userSchema);

Post Model

// models/Post.js
const mongoose = require('mongoose');

const postSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
status: {
type: String,
enum: ['draft', 'published'],
default: 'draft'
},
tags: [String]
}, {
timestamps: true
});

// Get published posts
postSchema.statics.getPublished = function() {
return this.find({ status: 'published' })
.populate('author', 'name email')
.sort({ createdAt: -1 });
};

module.exports = mongoose.model('Post', postSchema);

6. Authentication and Security

Auth Routes

// routes/auth.js
const express = require('express');
const User = require('../models/User');
const auth = require('../middleware/auth');
const router = express.Router();

// Register
router.post('/register', async (req, res) => {
try {
const { name, email, password } = 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 = new User({ name, email, password });
await user.save();

const token = user.generateToken();
res.status(201).json({ token, user: { id: user._id, name, email } });
} catch (error) {
res.status(400).json({ error: error.message });
}
});

// Login
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;

const user = await User.findOne({ email });
if (!user || !(await user.comparePassword(password))) {
return res.status(400).json({ error: 'Invalid credentials' });
}

const token = user.generateToken();
res.json({ token, user: { id: user._id, name: user.name, email } });
} catch (error) {
res.status(400).json({ error: error.message });
}
});

// Get current user
router.get('/me', auth, (req, res) => {
res.json(req.user);
});

module.exports = router;

7. API Development

Posts API

// routes/posts.js
const express = require('express');
const Post = require('../models/Post');
const auth = require('../middleware/auth');
const router = express.Router();

// GET /posts - Get all posts
router.get('/', async (req, res) => {
try {
const { status = 'published', page = 1, limit = 10 } = req.query;

const posts = await Post.find({ status })
.populate('author', 'name email')
.limit(limit * 1)
.skip((page - 1) * limit)
.sort({ createdAt: -1 });

res.json(posts);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// POST /posts - Create post
router.post('/', auth, async (req, res) => {
try {
const post = new Post({
...req.body,
author: req.user._id
});

await post.save();
await post.populate('author', 'name email');
res.status(201).json(post);
} catch (error) {
res.status(400).json({ error: error.message });
}
});

// GET /posts/:id - Get single post
router.get('/:id', async (req, res) => {
try {
const post = await Post.findById(req.params.id)
.populate('author', 'name email');

if (!post) {
return res.status(404).json({ error: 'Post not found' });
}

res.json(post);
} catch (error) {
res.status(500).json({ error: error.message });
}
});

// PUT /posts/:id - Update post
router.put('/:id', auth, async (req, res) => {
try {
const post = await Post.findOneAndUpdate(
{ _id: req.params.id, author: req.user._id },
req.body,
{ new: true }
).populate('author', 'name email');

if (!post) {
return res.status(404).json({ error: 'Post not found' });
}

res.json(post);
} catch (error) {
res.status(400).json({ error: error.message });
}
});

// DELETE /posts/:id - Delete post
router.delete('/:id', auth, async (req, res) => {
try {
const post = await Post.findOneAndDelete({
_id: req.params.id,
author: req.user._id
});

if (!post) {
return res.status(404).json({ error: 'Post not found' });
}

res.json({ message: 'Post deleted successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});

module.exports = router;

8. Error Handling

Global Error Handler

// middleware/errorHandler.js
const globalErrorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;

// Mongoose bad ObjectId
if (err.name === 'CastError') {
const message = 'Resource not found';
error = { message, statusCode: 404 };
}

// Mongoose duplicate key
if (err.code === 11000) {
const message = 'Duplicate field value entered';
error = { message, statusCode: 400 };
}

// Mongoose validation error
if (err.name === 'ValidationError') {
const message = Object.values(err.errors).map(val => val.message);
error = { message, statusCode: 400 };
}

res.status(error.statusCode || 500).json({
success: false,
error: error.message || 'Server Error'
});
};

// 404 handler
const notFound = (req, res) => {
res.status(404).json({ error: 'Route not found' });
};

module.exports = { globalErrorHandler, notFound };

9. Testing

API Testing with Jest

// tests/auth.test.js
const request = require('supertest');
const app = require('../server');
const User = require('../models/User');

describe('Auth Endpoints', () => {
beforeEach(async () => {
await User.deleteMany({});
});

describe('POST /auth/register', () => {
it('should register a new user', async () => {
const userData = {
name: 'John Doe',
email: '[email protected]',
password: 'password123'
};

const res = await request(app)
.post('/auth/register')
.send(userData)
.expect(201);

expect(res.body).toHaveProperty('token');
expect(res.body.user.email).toBe(userData.email);
});

it('should not register user with invalid email', async () => {
const userData = {
name: 'John Doe',
email: 'invalid-email',
password: 'password123'
};

await request(app)
.post('/auth/register')
.send(userData)
.expect(400);
});
});

describe('POST /auth/login', () => {
it('should login with valid credentials', async () => {
const user = new User({
name: 'John Doe',
email: '[email protected]',
password: 'password123'
});
await user.save();

const res = await request(app)
.post('/auth/login')
.send({
email: '[email protected]',
password: 'password123'
})
.expect(200);

expect(res.body).toHaveProperty('token');
});
});
});

10. Best Practices

Project Structure

my-express-app/
├── controllers/
├── middleware/
├── models/
├── routes/
├── tests/
├── utils/
├── .env
├── .gitignore
├── package.json
└── server.js

Security Best Practices

// Security setup in server.js
const helmet = require('helmet');
const cors = require('cors');

// Security middleware
app.use(helmet());
app.use(cors({
origin: process.env.FRONTEND_URL,
credentials: true
}));

// Request size limits
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ limit: '10mb', extended: true }));

// Rate limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);

Performance Optimization

// Database queries optimization
// Use select to limit fields
const users = await User.find().select('name email');

// Use populate efficiently
const posts = await Post.find()
.populate('author', 'name email')
.lean(); // Returns plain JavaScript objects

// Implement caching
const redis = require('redis');
const client = redis.createClient();

const getPostsFromCache = async (key) => {
const cachedPosts = await client.get(key);
return cachedPosts ? JSON.parse(cachedPosts) : null;
};

// Index important fields
userSchema.index({ email: 1 });
postSchema.index({ author: 1, status: 1 });

Environment Configuration

// config/config.js
module.exports = {
development: {
port: process.env.PORT || 3000,
database: process.env.DB_CONNECTION_STRING,
jwtSecret: process.env.JWT_SECRET
},
production: {
port: process.env.PORT,
database: process.env.DB_CONNECTION_STRING,
jwtSecret: process.env.JWT_SECRET
}
};

// Usage
const config = require('./config/config')[process.env.NODE_ENV || 'development'];

This comprehensive guide covers the essential aspects of Node.js and Express.js development, from basic setup to advanced features like authentication, database integration, and best practices. Follow these patterns to build robust and scalable web applications and APIs.