Express JS Full Course From Beginner to Pro 2025 | Complete Backend Tutorial in Just 4 Hours by GreatStack

This 4-hour video from GreatStack covers Express.js from basics to production, with a solid middleware section that shows patterns you'll actually use.

Key middleware timestamps:
- 57:32 - Middleware fundamentals and request/response cycle
- 2:51:11 - Authentication middleware patterns
- 3:43:55 - Error handling middleware (critical for production)

Why this video matters: Shows actual middleware patterns from production apps, not just hello-world bullshit. The authentication flows and error handling patterns are solid - worth watching to avoid common mistakes.

The middleware section at 57:32 is worth watching because it shows the request lifecycle and middleware ordering issues that will bite you later.

📺 YouTube

The Production Middleware Patterns That Actually Work

The Production Middleware Patterns That Actually Work

Building custom middleware is easy.

Building middleware that doesn't randomly break on Tuesday at 3am is hard. Here are the patterns that actually work when your app is getting hammered by real traffic.

Understanding the Middleware Chain (Where Everything Goes Wrong)

Express middleware is just a function with access to req, res, and next. The magic (and danger) is in the order. One middleware not calling next() and your request dies in middleware purgatory.

// This will kill your app silently
app.use((req, res, next) => {
  console.log('Request received');
  // Forgot next() 
- request hangs forever
});

// This actually works
app.use((req, res, next) => {
  console.log('Request received');
  next(); // ALWAYS call next() unless you're ending the response
});

The request flows through middleware in the exact order you define them. Authentication before route handlers. Error handling last. Fuck up the order and spend hours debugging why your auth isn't running. The Express routing guide explains middleware execution order in detail.

Custom Authentication Middleware (Real Production Pattern)

Here's how to build auth middleware that doesn't suck:

const jwt = require('jsonwebtoken');

const authenticate = (options = {}) => {
  return async (req, res, next) => {
    try {
      // Get token from multiple sources (headers, cookies, query)
      let token = req.headers.authorization?.replace('Bearer ', '') ||
                  req.cookies?.access_token ||
                  req.query?.token;

      if (!token) {
        if (options.optional) {
          req.user = null;
          return next();
        }
        return res.status(401).json({ error: 'Authentication required' });
      }

      // Verify and decode token
      const decoded = jwt.verify(token, process.env.

JWT_SECRET);
      
      // Optional: Check if user still exists in database
      if (options.checkUserExists) {
        const user = await User.findById(decoded.userId);
        if (!user) {
          return res.status(401).json({ error: 'User not found' });
        }
        req.user = user;
      } else {
        req.user = decoded;
      }

      next();
    } catch (error) {
      // JWT expired or invalid
      if (error.name === 'TokenExpiredError') {
        return res.status(401).json({ error: 'Token expired' });
      }
      if (error.name === 'JsonWebTokenError') {
        return res.status(401).json({ error: 'Invalid token' });
      }
      
      // Log unexpected errors but don't expose them
      console.error('Auth middleware error:', error);
      res.status(500).json({ error: 'Authentication error' });
    }
  };
};

// Usage 
- flexible for different requirements
app.use('/api/public', authenticate({ optional: true }));
app.use('/api/protected', authenticate({ checkUserExists: true }));

Key lessons:

Handle multiple token sources, make it configurable, don't leak error details, and always have fallbacks. For more JWT security best practices, check the OWASP JWT Cheat Sheet and Auth0's JWT guide.

Request Logging Middleware (Debug Your Prod Issues)

Logging middleware seems simple until you need to debug production issues.

Here's what actually helps:

const morgan = require('morgan');

const create

RequestLogger = (env = 'development') => {
  if (env === 'production') {
    // Structured logging for production 
- parseable by log aggregators
    return morgan('combined', {
      stream: {
        write: (message) => {
          const log = {
            timestamp: new Date().to

ISOString(),
            level: 'info',
            type: 'request',
            message: message.trim()
          };
          console.log(JSON.stringify(log));
        }
      }
    });
  } else {
    // Readable format for development
    return morgan('dev');
  }
};

// Custom request context middleware 
- adds request ID
const add

RequestContext = (req, res, next) => {
  req.requestId = Math.random().toString(36).substring(2);
  req.startTime = Date.now();
  
  // Add request ID to response headers for debugging
  res.set('X-Request-ID', req.requestId);
  
  next();
};

// Performance monitoring middleware
const trackPerformance = (req, res, next) => {
  const originalSend = res.send;
  
  res.send = function(data) {
    const responseTime = Date.now() 
- req.startTime;
    
    // Log slow requests (>1s) for investigation
    if (responseTime > 1000) {
      console.warn(`Slow request detected: ${req.method} ${req.path} 
- ${response

Time}ms`);
    }
    
    // Add performance headers
    res.set('X-Response-Time', `${responseTime}ms`);
    
    originalSend.call(this, data);
  };
  
  next();
};

app.use(addRequestContext);
app.use(createRequestLogger(process.env.

NODE_ENV));
app.use(trackPerformance);

This setup gives you request tracing, performance monitoring, and structured logs that actually help during outages. For production logging strategies, see Winston's best practices and The 12-Factor App logging guidelines.

Error Handling Middleware (The Final Safety Net)

Error middleware is your last chance to prevent crashes.

Express 5 catches promise rejections automatically, but you still need proper error boundaries:

// Async error wrapper 
- catches promise rejections
const async

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

// Validation error middleware 
- handles specific error types
const handleValidationError = (err, req, res, next) => {
  if (err.name === 'ValidationError') {
    const errors = Object.values(err.errors).map(e => e.message);
    return res.status(400).json({
      error: 'Validation failed',
      details: errors,
      requestId: req.request

Id
    });
  }
  next(err);
};

// Database error middleware 
- handles DB connection issues
const handleDatabaseError = (err, req, res, next) => {
  if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') {
    console.error('Database connection error:', err);
    return res.status(503).json({
      error: 'Service temporarily unavailable',
      requestId: req.request

Id
    });
  }
  next(err);
};

// Generic error handler 
- catches everything else
const genericErrorHandler = (err, req, res, next) => {
  // Log all errors with context
  console.error('Unhandled error:', {
    error: err.message,
    stack: err.stack,
    requestId: req.request

Id,
    url: req.url,
    method: req.method,
    userAgent: req.get('User-Agent'),
    timestamp: new Date().toISOString()
  });

  // Don't leak internal details in production
  if (process.env.

NODE_ENV === 'production') {
    res.status(500).json({
      error: 'Internal server error',
      requestId: req.requestId
    });
  } else {
    res.status(500).json({
      error: err.message,
      stack: err.stack,
      requestId: req.request

Id
    });
  }
};

// Order matters 
- specific handlers first, generic handler last
app.use(handleValidationError);
app.use(handleDatabaseError);
app.use(genericErrorHandler);

The key is layered error handling

  • catch specific cases first, then have a generic fallback that doesn't crash your app.

Rate Limiting Middleware (Prevent Your API From Getting Hammered)

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('redis');

// Redis-based rate limiting for multi-server deployments
const redisClient = Redis.createClient({
  host: process.env.

REDIS_HOST || 'localhost'
});

const createRateLimiter = (options) => {
  return rateLimit({
    store: new RedisStore({
      client: redis

Client,
      prefix: 'rate_limit:'
    }),
    windowMs: options.window

Ms || 15 * 60 * 1000, // 15 minutes
    max: options.max || 100, // requests per window
    message: {
      error: 'Too many requests',
      retryAfter:

 Math.ceil(options.window

Ms / 1000)
    },
    standardHeaders: true,
    legacyHeaders: false,
    // Skip rate limiting for whitelisted IPs
    skip: (req) => {
      const whitelist = process.env.

RATE_LIMIT_WHITELIST?.split(',') || [];
      return whitelist.includes(req.ip);
    }
  });
};

// Different limits for different endpoints
app.use('/api/auth', createRateLimiter({ max: 5, windowMs: 15 * 60 * 1000 })); // Stricter for auth
app.use('/api/', createRateLimiter({ max: 100 })); // General API limit

Use Redis for rate limiting if you have multiple servers, otherwise in-memory is fine for single instances.

For advanced rate limiting patterns, check the express-rate-limit documentation and Redis rate limiting patterns.

Middleware Patterns That'll Save Your Ass

**Pattern 1:

Conditional Middleware**

// Only apply middleware based on conditions
const conditional

Middleware = (condition, middleware) => {
  return (req, res, next) => {
    if (condition(req)) {
      middleware(req, res, next);
    } else {
      next();
    }
  };
};

// Usage: Only log requests to API endpoints
app.use(conditional

Middleware(
  req => req.path.startsWith('/api'),
  morgan('combined')
));

**Pattern 2:

Middleware Factories**

// Reusable middleware with configuration
const create

Validator = (schema) => {
  return (req, res, next) => {
    const { error } = schema.validate(req.body);
    if (error) {
      return res.status(400).json({
        error: 'Validation failed',
        details: error.details.map(d => d.message)
      });
    }
    next();
  };
};

// Usage
const userSchema = Joi.object({
  email:

 Joi.string().email().required(),
  password: Joi.string().min(8).required()
});

app.post('/users', create

Validator(userSchema), createUser);

**Pattern 3:

Middleware Composition**

// Combine multiple middleware into one
const compose

Middleware = (...middlewares) => {
  return (req, res, next) => {
    let index = 0;
    
    const dispatch = (i) => {
      if (i >= middlewares.length) return next();
      
      const middleware = middlewares[i];
      middleware(req, res, () => dispatch(i + 1));
    };
    
    dispatch(0);
  };
};

// Usage: Create authentication + authorization combo
const protectedRoute = composeMiddleware(
  authenticate({ checkUserExists: true }),
  authorize(['admin', 'moderator']),
  rateLimit({ max: 10 })
);

app.use('/admin', protected

Route);

These patterns let you build middleware that's reusable, testable, and doesn't break when requirements change.

For more middleware patterns, check Express middleware examples, [Node.js design patterns](https://github.com/Packt

Publishing/Node.js-Design-Patterns-Third-Edition), Express best practices, Middleware testing strategies, Error handling patterns, and Production middleware security.

Express Middleware Questions That Keep Developers Up at Night

Q

Why do my middlewares run in the wrong order?

A

Express runs middleware in the exact order you define them. If auth middleware runs after your routes, auth won't work. Place middleware BEFORE the routes that need them:

// WRONG - auth runs after routes
app.get('/protected', (req, res) => { /* needs auth */ });
app.use(authMiddleware);

// RIGHT - auth runs before routes  
app.use(authMiddleware);
app.get('/protected', (req, res) => { /* auth already checked */ });
Q

My custom middleware hangs requests - what's wrong?

A

You forgot to call next(). Every middleware must either send a response or call next() to continue the chain:

// This hangs forever
app.use((req, res, next) => {
  console.log('Request received');
  // Missing next() - request stuck in limbo
});

// This works
app.use((req, res, next) => {
  console.log('Request received');
  next(); // Continues to next middleware
});
Q

How do I handle errors in async middleware?

A

In Express 5, promise rejections are caught automatically. In Express 4, wrap async middleware with error handlers:

// Express 5 - automatic error handling
app.use(async (req, res, next) => {
  const user = await User.findById(req.params.id); // Auto-caught if throws
  req.user = user;
  next();
});

// Express 4 - manual error wrapping
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

app.use(asyncHandler(async (req, res, next) => {
  const user = await User.findById(req.params.id);
  req.user = user;
  next();
}));
Q

Can I apply middleware to specific routes only?

A

Yes, several ways:

// Apply to single route
app.get('/protected', authMiddleware, (req, res) => {
  res.json({ message: 'Protected data' });
});

// Apply to route group
app.use('/api/admin', authMiddleware);
app.get('/api/admin/users', getAllUsers);

// Apply conditionally
app.use((req, res, next) => {
  if (req.path.startsWith('/api/')) {
    authMiddleware(req, res, next);
  } else {
    next();
  }
});
Q

How do I debug middleware that's not running?

A

Check the order and path matching:

// Add debug middleware to see what's running
app.use('*', (req, res, next) => {
  console.log(`${req.method} ${req.path} - middleware running`);
  next();
});

// This won't match /api/users (missing trailing slash)
app.use('/api/', middleware);
app.get('/api/users', handler);

// This will match
app.use('/api', middleware);
app.get('/api/users', handler);
Q

Should I put all my middleware in one file?

A

Fuck no. Organize by purpose:

middleware/
├── auth.js          // Authentication middleware
├── validation.js    // Request validation  
├── logging.js       // Request/error logging
├── security.js      // Rate limiting, CORS, headers
└── index.js         // Import and compose all middleware
Q

My middleware is slowing down requests - how to fix?

A

Profile your middleware and fix the slow ones:

// Add timing to find slow middleware
app.use((req, res, next) => {
  const start = Date.now();
  req.startTime = start;
  
  const originalNext = next;
  next = function() {
    const duration = Date.now() - start;
    if (duration > 100) {
      console.warn(`Slow middleware: ${duration}ms`);
    }
    originalNext.apply(this, arguments);
  };
  
  next();
});

Common bottlenecks: Database queries in middleware, synchronous operations, and too many security headers.

Q

How do I share data between middleware?

A

Use the req object to store temporary data:

// Set data in first middleware
app.use((req, res, next) => {
  req.requestId = generateId();
  req.user = getCurrentUser();
  next();
});

// Use data in later middleware/routes
app.use((req, res, next) => {
  console.log(`Request ${req.requestId} from user ${req.user.id}`);
  next();
});

Don't use global variables - they'll break in multi-request scenarios.

Q

What's the difference between app.use() and router.use()?

A

app.use() applies to the entire application. router.use() applies only to that router's routes:

// Affects ALL routes
app.use(authMiddleware);

// Only affects routes in this router
const apiRouter = express.Router();
apiRouter.use(authMiddleware); // Only /api/* routes
apiRouter.get('/users', getUsers);
app.use('/api', apiRouter);
Q

My error middleware isn't catching errors - why?

A

Error middleware must have 4 parameters and be defined AFTER all routes:

// WRONG - missing error parameter
app.use((req, res, next) => {
  console.error('Error:', req.error);
});

// RIGHT - 4 parameters, defined at end
app.get('/routes', handler);
// ... all other routes

app.use((err, req, res, next) => {
  console.error('Error:', err);
  res.status(500).json({ error: 'Something went wrong' });
});
Q

How do I test custom middleware?

A

Use Supertest to test the full HTTP flow:

const request = require('supertest');
const app = require('../app');

describe('Auth Middleware', () => {
  it('should reject requests without token', async () => {
    await request(app)
      .get('/protected')
      .expect(401)
      .expect({ error: 'Authentication required' });
  });
  
  it('should allow requests with valid token', async () => {
    const token = generateValidToken();
    await request(app)
      .get('/protected')
      .set('Authorization', `Bearer ${token}`)
      .expect(200);
  });
});

Don't unit test middleware in isolation - test the actual HTTP behavior.

Express Middleware Approaches - What Actually Works in Production

Pattern

When to Use

Pros

Cons

Production Reality

Built-in Middleware

Standard functionality

✅ Well-tested
✅ Performance optimized
✅ Good docs

❌ Limited customization
❌ May include unused features

Use for basics (json, static, urlencoded)

Third-party Middleware

Common patterns (auth, logging, security)

✅ Community tested
✅ Feature rich
✅ Regular updates

❌ Dependency bloat
❌ Security risks
❌ Breaking changes

Vet carefully

  • check GitHub activity

Custom Middleware

Specific business logic

✅ Full control
✅ No dependencies
✅ Tailored to needs

❌ More code to maintain
❌ Potential bugs
❌ Testing overhead

Essential for unique requirements

Middleware Factories

Reusable patterns with config

✅ DRY principle
✅ Flexible
✅ Testable

❌ Added complexity
❌ Harder to debug

Great for auth, validation, logging

Conditional Middleware

Route-specific middleware

✅ Performance gains
✅ Cleaner separation

❌ Logic complexity
❌ Harder to trace

Perfect for optional features

Express Middleware Resources That Don't Waste Your Time

Related Tools & Recommendations

integration
Similar content

Claude API Node.js Express: Advanced Code Execution & Tools Guide

Build production-ready applications with Claude's code execution and file processing tools

Claude API
/integration/claude-api-nodejs-express/advanced-tools-integration
100%
howto
Similar content

Install Node.js & NVM on Mac M1/M2/M3: A Complete Guide

My M1 Mac setup broke at 2am before a deployment. Here's how I fixed it so you don't have to suffer.

Node Version Manager (NVM)
/howto/install-nodejs-nvm-mac-m1/complete-installation-guide
87%
review
Recommended

Which JavaScript Runtime Won't Make You Hate Your Life

Two years of runtime fuckery later, here's the truth nobody tells you

Bun
/review/bun-nodejs-deno-comparison/production-readiness-assessment
85%
tool
Similar content

Express.js - The Web Framework Nobody Wants to Replace

It's ugly, old, and everyone still uses it

Express.js
/tool/express/overview
68%
compare
Recommended

PostgreSQL vs MySQL vs MongoDB vs Cassandra - Which Database Will Ruin Your Weekend Less?

Skip the bullshit. Here's what breaks in production.

PostgreSQL
/compare/postgresql/mysql/mongodb/cassandra/comprehensive-database-comparison
54%
tool
Similar content

Express.js API Development Patterns: Build Robust REST APIs

REST patterns, validation, auth flows, and error handling that actually work in production

Express.js
/tool/express/api-development-patterns
54%
integration
Similar content

Claude API Node.js Express Integration: Complete Guide

Stop fucking around with tutorials that don't work in production

Claude API
/integration/claude-api-nodejs-express/complete-implementation-guide
48%
integration
Similar content

MongoDB Express Mongoose Production: Deployment & Troubleshooting

Deploy Without Breaking Everything (Again)

MongoDB
/integration/mongodb-express-mongoose/production-deployment-guide
48%
tool
Similar content

Node.js Overview: JavaScript Runtime, Production Tips & FAQs

Explore Node.js: understand this powerful JavaScript runtime, learn essential production best practices, and get answers to common questions about its performan

Node.js
/tool/node.js/overview
47%
pricing
Recommended

What Enterprise Platform Pricing Actually Looks Like When the Sales Gloves Come Off

Vercel, Netlify, and Cloudflare Pages: The Real Costs Behind the Marketing Bullshit

Vercel
/pricing/vercel-netlify-cloudflare-enterprise-comparison/enterprise-cost-analysis
45%
pricing
Recommended

Got Hit With a $3k Vercel Bill Last Month: Real Platform Costs

These platforms will fuck your budget when you least expect it

Vercel
/pricing/vercel-vs-netlify-vs-cloudflare-pages/complete-pricing-breakdown
45%
tool
Similar content

Node.js Security Hardening Guide: Protect Your Apps

Master Node.js security hardening. Learn to manage npm dependencies, fix vulnerabilities, implement secure authentication, HTTPS, and input validation.

Node.js
/tool/node.js/security-hardening
44%
troubleshoot
Recommended

Fix Kubernetes Service Not Accessible - Stop the 503 Hell

Your pods show "Running" but users get connection refused? Welcome to Kubernetes networking hell.

Kubernetes
/troubleshoot/kubernetes-service-not-accessible/service-connectivity-troubleshooting
42%
tool
Similar content

npm - The Package Manager Everyone Uses But Nobody Really Likes

It's slow, it breaks randomly, but it comes with Node.js so here we are

npm
/tool/npm/overview
41%
tool
Similar content

Node.js ESM Migration: Upgrade CommonJS to ES Modules Safely

How to migrate from CommonJS to ESM without your production apps shitting the bed

Node.js
/tool/node.js/modern-javascript-migration
40%
tool
Similar content

Node.js Microservices: Avoid Pitfalls & Build Robust Systems

Learn why Node.js microservices projects often fail and discover practical strategies to build robust, scalable distributed systems. Avoid common pitfalls and e

Node.js
/tool/node.js/microservices-architecture
36%
tool
Similar content

Docker: Package Code, Run Anywhere - Fix 'Works on My Machine'

No more "works on my machine" excuses. Docker packages your app with everything it needs so it runs the same on your laptop, staging, and prod.

Docker Engine
/tool/docker/overview
36%
tool
Similar content

Node.js Production Deployment - How to Not Get Paged at 3AM

Optimize Node.js production deployment to prevent outages. Learn common pitfalls, PM2 clustering, troubleshooting FAQs, and effective monitoring for robust Node

Node.js
/tool/node.js/production-deployment
36%
tool
Similar content

Node.js Memory Leaks & Debugging: Stop App Crashes

Learn to identify and debug Node.js memory leaks, prevent 'heap out of memory' errors, and keep your applications stable. Explore common patterns, tools, and re

Node.js
/tool/node.js/debugging-memory-leaks
36%
tool
Similar content

Node.js Testing Strategies: Jest, Vitest & Integration Tests

Explore Node.js testing strategies, comparing Jest, Vitest, and native runners. Learn about crucial integration testing, troubleshoot CI failures, and optimize

Node.js
/tool/node.js/testing-strategies
36%

Recommendations combine user behavior, content similarity, research intelligence, and SEO optimization