Skip to main content

Node.js & Express - Building Scalable Backend Applications

ยท 10 min read
Shutiye Dev
Software Engineer & Educator

Node.js has revolutionized server-side JavaScript development, and Express.js makes it incredibly easy to build powerful web applications and APIs. Let's dive into building a production-ready backend application!

๐ŸŒŸ Why Node.js & Express?โ€‹

Node.js and Express dominate the backend JavaScript ecosystem:

  • โšก Non-blocking I/O: Handle thousands of concurrent connections
  • ๐Ÿ”„ JavaScript Everywhere: Use the same language for frontend and backend
  • ๐Ÿ“ฆ NPM Ecosystem: Access to 2+ million packages
  • ๐Ÿš€ Fast Development: Build and deploy quickly
  • ๐Ÿ’ช Scalable: Perfect for microservices and real-time applications
  • ๐Ÿ‘ฅ Active Community: Massive community support and resources

When to Use Node.jsโ€‹

โœ… Perfect for:

  • Real-time applications (chat, gaming, collaboration)
  • RESTful APIs and microservices
  • Single-page applications
  • Streaming applications
  • IoT applications

โŒ Not ideal for:

  • CPU-intensive operations
  • Heavy computational tasks

๐Ÿ“ฆ Getting Startedโ€‹

Prerequisitesโ€‹

# Check Node.js version (should be 18+)
node --version

# Check npm version
npm --version

# If not installed, download from nodejs.org

Project Setupโ€‹

# Create project directory
mkdir express-ecommerce-api
cd express-ecommerce-api

# Initialize npm project
npm init -y

# Install dependencies
npm install express mongoose dotenv cors helmet morgan
npm install --save-dev nodemon

# Install development tools
npm install --save-dev @types/node @types/express

๐Ÿ—๏ธ Project Structureโ€‹

Create a professional, scalable structure:

express-ecommerce-api/
โ”œโ”€โ”€ src/
โ”‚ โ”œโ”€โ”€ config/
โ”‚ โ”‚ โ””โ”€โ”€ database.js
โ”‚ โ”œโ”€โ”€ controllers/
โ”‚ โ”‚ โ”œโ”€โ”€ productController.js
โ”‚ โ”‚ โ””โ”€โ”€ orderController.js
โ”‚ โ”œโ”€โ”€ models/
โ”‚ โ”‚ โ”œโ”€โ”€ Product.js
โ”‚ โ”‚ โ””โ”€โ”€ Order.js
โ”‚ โ”œโ”€โ”€ routes/
โ”‚ โ”‚ โ”œโ”€โ”€ productRoutes.js
โ”‚ โ”‚ โ””โ”€โ”€ orderRoutes.js
โ”‚ โ”œโ”€โ”€ middleware/
โ”‚ โ”‚ โ”œโ”€โ”€ errorHandler.js
โ”‚ โ”‚ โ””โ”€โ”€ validateRequest.js
โ”‚ โ”œโ”€โ”€ utils/
โ”‚ โ”‚ โ””โ”€โ”€ logger.js
โ”‚ โ””โ”€โ”€ app.js
โ”œโ”€โ”€ .env
โ”œโ”€โ”€ .gitignore
โ”œโ”€โ”€ package.json
โ””โ”€โ”€ server.js

๐ŸŽฏ Building an E-Commerce APIโ€‹

Let's build a complete e-commerce API with products and orders.

Step 1: Environment Configurationโ€‹

Create .env:

# Server Configuration
PORT=5000
NODE_ENV=development

# Database
MONGODB_URI=mongodb://localhost:27017/ecommerce
# Or use MongoDB Atlas:
# MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/ecommerce

# JWT Secret
JWT_SECRET=your_super_secret_key_change_in_production

# API Configuration
API_VERSION=v1

Create .gitignore:

node_modules/
.env
*.log
dist/
build/

Step 2: Database Configurationโ€‹

Create src/config/database.js:

const mongoose = require('mongoose');

const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});

console.log(`โœ… MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error(`โŒ Error: ${error.message}`);
process.exit(1);
}
};

module.exports = connectDB;

Step 3: Create Modelsโ€‹

Create src/models/Product.js:

const mongoose = require('mongoose');

const productSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, 'Product name is required'],
trim: true,
maxlength: [100, 'Product name cannot exceed 100 characters'],
},
description: {
type: String,
required: [true, 'Product description is required'],
maxlength: [2000, 'Description cannot exceed 2000 characters'],
},
price: {
type: Number,
required: [true, 'Product price is required'],
min: [0, 'Price cannot be negative'],
},
category: {
type: String,
required: [true, 'Product category is required'],
enum: ['Electronics', 'Clothing', 'Books', 'Home', 'Sports', 'Other'],
},
stock: {
type: Number,
required: true,
min: [0, 'Stock cannot be negative'],
default: 0,
},
images: [{
type: String,
}],
rating: {
type: Number,
default: 0,
min: 0,
max: 5,
},
numReviews: {
type: Number,
default: 0,
},
featured: {
type: Boolean,
default: false,
},
},
{
timestamps: true,
}
);

// Index for better search performance
productSchema.index({ name: 'text', description: 'text' });

module.exports = mongoose.model('Product', productSchema);

Create src/models/Order.js:

const mongoose = require('mongoose');

const orderSchema = new mongoose.Schema(
{
orderNumber: {
type: String,
required: true,
unique: true,
},
customer: {
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
},
phone: {
type: String,
required: true,
},
},
items: [{
product: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Product',
required: true,
},
quantity: {
type: Number,
required: true,
min: 1,
},
price: {
type: Number,
required: true,
},
}],
shippingAddress: {
street: String,
city: String,
state: String,
zipCode: String,
country: String,
},
totalAmount: {
type: Number,
required: true,
},
status: {
type: String,
enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled'],
default: 'pending',
},
paymentStatus: {
type: String,
enum: ['pending', 'completed', 'failed'],
default: 'pending',
},
},
{
timestamps: true,
}
);

// Generate order number before saving
orderSchema.pre('save', async function(next) {
if (!this.orderNumber) {
this.orderNumber = `ORD-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
}
next();
});

module.exports = mongoose.model('Order', orderSchema);

Step 4: Create Controllersโ€‹

Create src/controllers/productController.js:

const Product = require('../models/Product');

// @desc Get all products
// @route GET /api/v1/products
// @access Public
exports.getProducts = async (req, res, next) => {
try {
const { page = 1, limit = 10, category, search, sort } = req.query;

// Build query
const query = {};
if (category) query.category = category;
if (search) query.$text = { $search: search };

// Build sort
let sortOption = {};
if (sort === 'price_asc') sortOption.price = 1;
else if (sort === 'price_desc') sortOption.price = -1;
else if (sort === 'rating') sortOption.rating = -1;
else sortOption.createdAt = -1;

// Execute query with pagination
const products = await Product.find(query)
.sort(sortOption)
.limit(limit * 1)
.skip((page - 1) * limit)
.exec();

const count = await Product.countDocuments(query);

res.status(200).json({
success: true,
count: products.length,
total: count,
page: Number(page),
pages: Math.ceil(count / limit),
data: products,
});
} catch (error) {
next(error);
}
};

// @desc Get single product
// @route GET /api/v1/products/:id
// @access Public
exports.getProduct = async (req, res, next) => {
try {
const product = await Product.findById(req.params.id);

if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found',
});
}

res.status(200).json({
success: true,
data: product,
});
} catch (error) {
next(error);
}
};

// @desc Create product
// @route POST /api/v1/products
// @access Private/Admin
exports.createProduct = async (req, res, next) => {
try {
const product = await Product.create(req.body);

res.status(201).json({
success: true,
data: product,
});
} catch (error) {
next(error);
}
};

// @desc Update product
// @route PUT /api/v1/products/:id
// @access Private/Admin
exports.updateProduct = async (req, res, next) => {
try {
const product = await Product.findByIdAndUpdate(
req.params.id,
req.body,
{
new: true,
runValidators: true,
}
);

if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found',
});
}

res.status(200).json({
success: true,
data: product,
});
} catch (error) {
next(error);
}
};

// @desc Delete product
// @route DELETE /api/v1/products/:id
// @access Private/Admin
exports.deleteProduct = async (req, res, next) => {
try {
const product = await Product.findByIdAndDelete(req.params.id);

if (!product) {
return res.status(404).json({
success: false,
message: 'Product not found',
});
}

res.status(200).json({
success: true,
message: 'Product deleted successfully',
});
} catch (error) {
next(error);
}
};

// @desc Get featured products
// @route GET /api/v1/products/featured
// @access Public
exports.getFeaturedProducts = async (req, res, next) => {
try {
const products = await Product.find({ featured: true })
.limit(10)
.sort('-rating');

res.status(200).json({
success: true,
count: products.length,
data: products,
});
} catch (error) {
next(error);
}
};

Step 5: Create Routesโ€‹

Create src/routes/productRoutes.js:

const express = require('express');
const {
getProducts,
getProduct,
createProduct,
updateProduct,
deleteProduct,
getFeaturedProducts,
} = require('../controllers/productController');

const router = express.Router();

router.get('/featured', getFeaturedProducts);
router.route('/')
.get(getProducts)
.post(createProduct);

router.route('/:id')
.get(getProduct)
.put(updateProduct)
.delete(deleteProduct);

module.exports = router;

Step 6: Middlewareโ€‹

Create src/middleware/errorHandler.js:

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

// Log to console for dev
console.error(err);

// 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',
});
};

module.exports = errorHandler;

Step 7: Main Applicationโ€‹

Create src/app.js:

const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const errorHandler = require('./middleware/errorHandler');

// Import routes
const productRoutes = require('./routes/productRoutes');

const app = express();

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

// Logging
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}

// Health check
app.get('/health', (req, res) => {
res.status(200).json({
success: true,
message: 'Server is healthy',
timestamp: new Date().toISOString(),
});
});

// API routes
app.use('/api/v1/products', productRoutes);

// Root route
app.get('/', (req, res) => {
res.json({
message: 'Welcome to E-Commerce API',
version: '1.0.0',
endpoints: {
products: '/api/v1/products',
health: '/health',
},
});
});

// Error handler (must be last)
app.use(errorHandler);

// 404 handler
app.use((req, res) => {
res.status(404).json({
success: false,
message: 'Route not found',
});
});

module.exports = app;

Create server.js:

require('dotenv').config();
const app = require('./src/app');
const connectDB = require('./src/config/database');

const PORT = process.env.PORT || 5000;

// Connect to database
connectDB();

// Start server
const server = app.listen(PORT, () => {
console.log(`๐Ÿš€ Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
});

// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
console.log(`โŒ Error: ${err.message}`);
server.close(() => process.exit(1));
});

Update package.json:

{
"name": "express-ecommerce-api",
"version": "1.0.0",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
}

๐Ÿš€ Running the Applicationโ€‹

# Development mode with auto-reload
npm run dev

# Production mode
npm start

Your API will be available at http://localhost:5000

๐Ÿงช Testing the APIโ€‹

Create a Productโ€‹

curl -X POST http://localhost:5000/api/v1/products \
-H "Content-Type: application/json" \
-d '{
"name": "MacBook Pro 16-inch",
"description": "Powerful laptop for developers",
"price": 2499,
"category": "Electronics",
"stock": 50,
"featured": true
}'

Get All Productsโ€‹

# Basic request
curl http://localhost:5000/api/v1/products

# With pagination
curl "http://localhost:5000/api/v1/products?page=1&limit=10"

# Filter by category
curl "http://localhost:5000/api/v1/products?category=Electronics"

# Search products
curl "http://localhost:5000/api/v1/products?search=laptop"

# Sort by price
curl "http://localhost:5000/api/v1/products?sort=price_asc"

โšก Advanced Featuresโ€‹

1. Request Validationโ€‹

Install validator:

npm install express-validator

Create validation middleware:

const { body, validationResult } = require('express-validator');

exports.validateProduct = [
body('name').trim().notEmpty().withMessage('Name is required'),
body('price').isNumeric().withMessage('Price must be a number'),
body('stock').isInt({ min: 0 }).withMessage('Stock must be non-negative'),

(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
},
];

2. Rate Limitingโ€‹

npm install express-rate-limit
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
message: 'Too many requests, please try again later',
});

app.use('/api/', limiter);

3. Async Handler Wrapperโ€‹

const asyncHandler = fn => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};

// Use it
exports.getProducts = asyncHandler(async (req, res) => {
const products = await Product.find();
res.json({ data: products });
});

4. File Uploadโ€‹

npm install multer
const multer = require('multer');

const storage = multer.diskStorage({
destination: './uploads/',
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
},
});

const upload = multer({ storage });

app.post('/upload', upload.single('image'), (req, res) => {
res.json({ filename: req.file.filename });
});

๐Ÿ”’ Security Best Practicesโ€‹

1. Environment Variablesโ€‹

Never commit .env files. Use different configs for different environments.

2. Input Sanitizationโ€‹

npm install express-mongo-sanitize
const mongoSanitize = require('express-mongo-sanitize');
app.use(mongoSanitize());

3. XSS Protectionโ€‹

npm install xss-clean
const xss = require('xss-clean');
app.use(xss());

4. HTTP Security Headersโ€‹

Already using helmet:

app.use(helmet());

๐Ÿ“Š Performance Optimizationโ€‹

1. Caching with Redisโ€‹

npm install redis
const redis = require('redis');
const client = redis.createClient();

// Cache middleware
const cache = (req, res, next) => {
const key = req.originalUrl;
client.get(key, (err, data) => {
if (data) {
return res.json(JSON.parse(data));
}
next();
});
};

2. Database Indexingโ€‹

// In your model
productSchema.index({ name: 1, category: 1 });

3. Compressionโ€‹

npm install compression
const compression = require('compression');
app.use(compression());

๐Ÿš€ Deploymentโ€‹

Using PM2โ€‹

npm install -g pm2

# Start app
pm2 start server.js --name "ecommerce-api"

# Monitor
pm2 monit

# Restart
pm2 restart ecommerce-api

Dockerโ€‹

Create Dockerfile:

FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 5000

CMD ["node", "server.js"]

๐ŸŽ“ Key Takeawaysโ€‹

  • Express is minimal: You have full control over your application structure
  • Middleware pattern: Powerful and flexible request processing
  • Async/await: Modern JavaScript for handling asynchronous operations
  • Mongoose: Elegant MongoDB object modeling
  • Security: Always implement security best practices
  • Scalability: Node.js excels at handling concurrent connections

๐Ÿ“š What's Next?โ€‹

  • Add JWT authentication
  • Implement WebSocket for real-time features
  • Add GraphQL support
  • Integrate payment gateways (Stripe, PayPal)
  • Set up testing with Jest
  • Deploy to AWS, Heroku, or DigitalOcean

Node.js and Express provide a solid foundation for building modern, scalable backend applications. Start building your APIs today!


Want to learn more? Check out our backend development curriculum or connect with us on Twitter!