Next.js Custom Server
Introduction
By default, Next.js provides a built-in server that handles routing, rendering, and other core functionalities. However, there are scenarios where you might need more control over the server behavior. This is where a Custom Server comes into play.
A Custom Server allows you to:
- Start your Next.js app programmatically
- Use custom server-side routing logic
- Connect to server-side databases directly
- Integrate with existing Node.js applications or Express.js servers
- Implement custom middleware for authentication, logging, etc.
However, it's important to note that using a custom server comes with some trade-offs. You'll lose key Next.js features like:
- Automatic static optimization
- Faster builds
- Smaller server bundles
- Serverless deployment options
Let's dive into how to set up a custom server and explore different use cases!
Setting Up a Basic Custom Server
To create a custom server, you need to create a new file (commonly named server.js) at the root of your project:
// server.js
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
  createServer((req, res) => {
    const parsedUrl = parse(req.url, true);
    handle(req, res, parsedUrl);
  }).listen(3000, (err) => {
    if (err) throw err;
    console.log('> Ready on http://localhost:3000');
  });
});
After creating this file, you'll need to modify your package.json to use this custom server instead of the default Next.js server:
{
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
  }
}
Now when you run npm run dev or yarn dev, your Next.js application will use your custom server implementation instead of the default one.
Custom Routing with a Custom Server
One of the main benefits of using a custom server is implementing custom routing logic. Here's an example that handles a custom route pattern:
// server.js
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
  createServer((req, res) => {
    const parsedUrl = parse(req.url, true);
    const { pathname, query } = parsedUrl;
    
    // Custom routing rules
    if (pathname === '/products') {
      app.render(req, res, '/product-list', query);
    } else if (pathname.startsWith('/p/')) {
      // Extract product ID from URL
      const id = pathname.split('/')[2];
      app.render(req, res, '/product', { id, ...query });
    } else {
      handle(req, res, parsedUrl);
    }
  }).listen(3000, (err) => {
    if (err) throw err;
    console.log('> Ready on http://localhost:3000');
  });
});
In this example:
- When users visit /products, they'll see the content from/product-listpage
- When users visit /p/123, they'll see the content from/productpage with the ID parameter set to "123"
- All other routes are handled by Next.js's default routing system
Using Express.js with Next.js
Express is a popular Node.js web framework that you can integrate with Next.js for more complex server-side logic:
// server.js
const express = require('express');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
  const server = express();
  
  // Middleware example
  server.use((req, res, next) => {
    console.log(`Request to: ${req.url}`);
    next();
  });
  
  // Custom API endpoint
  server.get('/api/hello', (req, res) => {
    res.json({ message: 'Hello from custom server!' });
  });
  
  // Custom page routing
  server.get('/products/:id', (req, res) => {
    return app.render(req, res, '/product', { id: req.params.id });
  });
  
  // Let Next.js handle all other routes
  server.all('*', (req, res) => {
    return handle(req, res);
  });
  
  server.listen(3000, (err) => {
    if (err) throw err;
    console.log('> Ready on http://localhost:3000');
  });
});
With this setup, you can:
- Use Express middleware
- Create custom API endpoints directly in your server
- Use Express's routing system alongside Next.js
Real-World Example: Authentication Middleware
Here's a more practical example demonstrating how to implement basic authentication middleware with a custom server:
// server.js
const express = require('express');
const next = require('next');
const session = require('express-session');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
// Mock authentication data
const users = {
  'user1': { password: 'password1', name: 'User One' },
  'user2': { password: 'password2', name: 'User Two' }
};
app.prepare().then(() => {
  const server = express();
  
  // Set up session middleware
  server.use(
    session({
      secret: 'my-secret-key',
      resave: false,
      saveUninitialized: false,
    })
  );
  
  // Parse JSON request bodies
  server.use(express.json());
  
  // Login endpoint
  server.post('/api/login', (req, res) => {
    const { username, password } = req.body;
    
    if (users[username] && users[username].password === password) {
      req.session.user = {
        username,
        name: users[username].name,
      };
      res.status(200).json({ success: true });
    } else {
      res.status(401).json({ success: false, message: 'Invalid credentials' });
    }
  });
  
  // Authentication middleware for protected routes
  const requireAuth = (req, res, next) => {
    if (req.session.user) {
      next();
    } else {
      res.redirect('/login');
    }
  };
  
  // Protected route example
  server.get('/dashboard', requireAuth, (req, res) => {
    return app.render(req, res, '/dashboard', { user: req.session.user });
  });
  
  // Handle all other routes
  server.all('*', (req, res) => {
    return handle(req, res);
  });
  
  server.listen(3000, (err) => {
    if (err) throw err;
    console.log('> Ready on http://localhost:3000');
  });
});
In this example, we've implemented:
- Session handling with express-session
- A login API endpoint that authenticates users
- A middleware function that protects certain routes
- A protected dashboard route that requires authentication
Custom Server with WebSockets
Another powerful use case for a custom server is implementing real-time functionality with WebSockets:
// server.js
const express = require('express');
const http = require('http');
const next = require('next');
const WebSocket = require('ws');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
  const server = express();
  
  // Regular HTTP routes
  server.all('*', (req, res) => {
    return handle(req, res);
  });
  
  // Create HTTP server
  const httpServer = http.createServer(server);
  
  // Set up WebSocket server
  const wss = new WebSocket.Server({ server: httpServer });
  
  // WebSocket connection handling
  wss.on('connection', (ws) => {
    console.log('Client connected');
    
    // Send welcome message
    ws.send(JSON.stringify({ 
      type: 'connection', 
      message: 'Connected to WebSocket server' 
    }));
    
    // Handle messages from client
    ws.on('message', (message) => {
      console.log('Received:', message);
      
      // Echo the message back
      ws.send(JSON.stringify({
        type: 'echo',
        message: `Echo: ${message}`
      }));
    });
    
    ws.on('close', () => {
      console.log('Client disconnected');
    });
  });
  
  // Start server
  httpServer.listen(3000, (err) => {
    if (err) throw err;
    console.log('> Ready on http://localhost:3000');
  });
});
On the client side, you can connect to this WebSocket server like this:
// pages/index.js
import { useState, useEffect } from 'react';
export default function Home() {
  const [messages, setMessages] = useState([]);
  const [inputValue, setInputValue] = useState('');
  const [socket, setSocket] = useState(null);
  useEffect(() => {
    // Create WebSocket connection
    const ws = new WebSocket(`ws://${window.location.host}`);
    
    ws.onopen = () => {
      console.log('Connected to WebSocket');
      setSocket(ws);
    };
    
    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      setMessages((prev) => [...prev, data]);
    };
    
    ws.onclose = () => {
      console.log('Disconnected from WebSocket');
    };
    
    // Clean up on unmount
    return () => {
      ws.close();
    };
  }, []);
  
  const sendMessage = (e) => {
    e.preventDefault();
    if (socket && inputValue) {
      socket.send(inputValue);
      setInputValue('');
    }
  };
  
  return (
    <div>
      <h1>WebSocket Demo</h1>
      
      <div style={{ marginBottom: 20 }}>
        <form onSubmit={sendMessage}>
          <input 
            type="text" 
            value={inputValue}
            onChange={(e) => setInputValue(e.target.value)}
            placeholder="Type a message"
          />
          <button type="submit">Send</button>
        </form>
      </div>
      
      <div style={{ border: '1px solid #ccc', padding: 10, height: 300, overflowY: 'auto' }}>
        {messages.map((msg, index) => (
          <div key={index} style={{ marginBottom: 10 }}>
            <strong>{msg.type}:</strong> {msg.message}
          </div>
        ))}
      </div>
    </div>
  );
}
Best Practices and Considerations
When implementing a custom server for your Next.js application, keep these best practices in mind:
- 
Performance Impact: Custom servers disable some Next.js optimizations. Only use them when necessary. 
- 
Code Organization: Keep your server code modular and well-organized. 
- 
Environment Variables: Use environment variables for configuration rather than hardcoding values. 
- 
Error Handling: Implement proper error handling to prevent server crashes. 
- 
Deployment Considerations: A custom server requires a Node.js environment for deployment and can't use serverless deployments. 
- 
Security: Be careful when implementing authentication, rate limiting, and other security-related features. 
Example of better error handling:
// server.js with improved error handling
const express = require('express');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
app.prepare().then(() => {
  const server = express();
  
  // Global error handler middleware
  server.use((err, req, res, next) => {
    console.error('Server error:', err);
    res.status(500).send('Something broke on our end! Please try again later.');
  });
  
  // Handle uncaught exceptions
  process.on('uncaughtException', (err) => {
    console.error('Uncaught exception:', err);
    // Consider implementing graceful shutdown here
  });
  
  process.on('unhandledRejection', (reason, promise) => {
    console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  });
  
  // Route handling
  server.all('*', (req, res) => {
    return handle(req, res).catch(err => {
      console.error('Error handling request:', err);
      res.status(500).send('Server error');
    });
  });
  
  // Use environment variable for port
  const PORT = process.env.PORT || 3000;
  server.listen(PORT, (err) => {
    if (err) throw err;
    console.log(`> Ready on http://localhost:${PORT}`);
  });
});
Summary
Next.js custom servers provide powerful flexibility for advanced use cases where the default server behavior isn't sufficient. We've learned:
- How to set up a basic custom server
- Implementing custom routing logic
- Integrating with Express.js for middleware and advanced routing
- Creating real-world applications with authentication
- Adding WebSocket support for real-time functionality
- Best practices for deploying and maintaining custom servers
While custom servers are powerful, remember that they come with trade-offs. The Next.js team recommends using the built-in functionality whenever possible, and only reaching for custom servers when you need the extra flexibility they provide.
Additional Resources
Exercises
- 
Build a custom Next.js server with Express that includes rate limiting middleware for API routes. 
- 
Create a simple chat application using Next.js with a WebSocket server. 
- 
Implement a custom server that connects to a database (like MongoDB) and adds server-side caching. 
- 
Build a custom authentication system with session management using a custom server. 
- 
Create a proxy server using a Next.js custom server that forwards requests to different backend services based on the route. 
Happy coding!
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!