Custom Next.js Servers - Do you really need them?
If you stumbled upon this post, chances are you are either using custom servers in one of your Next.js projects or considering it. You may also just be curious about custom servers and whether you need them.
In either case, here are some things you should know about using custom servers in Next.js.
What are custom servers?
By default, Next.js uses a built-in server to handle incoming requests, which automatically starts when you run next dev or next start. However, if you need more control — such as handling specialized routing patterns or implementing custom server-side behaviors — you have the option to create a custom server. This approach gives you the flexibility to programmatically manage request handling beyond what Next.js’s native routing offers.
To opt out of the default server, you need to create a server.js file in the root of your project and modify the package.json to use it:
`
A basic custom server implementation using the Node.js HTTP module for some custom routing might look like this:
`
In this example:
- We create a Next.js app instance with next()
- We get the request handler with app.getRequestHandler()
- We create a custom HTTP server that intercepts requests to /custom-route
- For that route, we render a specific page with custom query parameters
- Next.js's default handler handles all other routes
And here's an example of a custom server implementation that uses Express.js:
`
In this Express.js example:
- We create an Express server instance
- We add custom middleware for logging requests
- We define a parameterized route /custom-route/:id that renders a specific page
- We create a custom API endpoint at /api/custom
- We use a catch-all route to let Next.js handle all other routes
Common use cases for custom servers
Some of the common reasons why people have used custom servers include:
1. Custom Routing: Sometimes, you might want to implement custom routing logic beyond what Next.js's file-based routing offers, such as custom URL patterns and parameters or supporting legacy URL structures.
2. Request/Response Manipulation: Another reason is adding custom headers to responses or modifying request objects before they reach the application, such as implementing custom CORS policies.
3. Authentication and Authorization: Custom authentication flows can sometimes be implemented in a custom server, such as protecting routes based on user role or managing session state.
4. WebSockets Support: If your application requires real-time communication, you might need to implement WebSockets support, e.g., chat applications or live notifications and updates. That has been one of the most common reasons for using custom servers.
5. Background Processing: If you're using logging tools such as New Relic, you might need to implement background processing to avoid blocking responses. This is where custom servers could be useful historically.
6. Proxying Requests: Having integration with external APIs or services, you might need a custom server to be able to forward requests to those APIs, implement API gateways, or avoid CORS issues with these third-party services.
7. Non-HTTP Protocol Support: If your application needs to support protocols beyond HTTP and WebSockets that aren't supported by the Edge Runtime, having a custom server could be a solution.
8. Integration with Existing Systems: Embedding Next.js within larger applications, integrating with proprietary middleware, or working with legacy enterprise systems may require a custom server implementation.
9. Specialized Performance Requirements: Specific performance requirements, such as custom connection pooling or server-level caching strategies, may warrant a solution implemented in a custom server.
10. Multi-Application Architectures: A custom server might be needed if you need to serve multiple Next.js applications from a single server with custom routing logic or implement application-level load balancing.
Caveats and Considerations of Using Custom Servers
You should be aware of several important implications of using custom servers.
Performance Implications
1. Loss of Automatic Static Optimization: Custom servers disable Next.js's automatic static optimization, forcing all pages to be server-rendered at runtime even if they could be statically generated. This can significantly impact performance and increase server load.
2. Increased TTFB (Time to First Byte): Without static optimization, Time to First Byte typically increases, affecting core web vitals and user experience.
3. Reduced Edge Caching Opportunities: Custom servers may interfere with CDN caching strategies that Next.js would otherwise optimize automatically.
Deployment Limitations
1. Vercel Incompatibility: Custom servers cannot be deployed on Vercel, eliminating access to Vercel's optimized infrastructure for Next.js applications.
2. Serverless Deployment Challenges: Many serverless platforms are incompatible with custom server implementations, limiting deployment options.
3. Increased Infrastructure Requirements: Custom servers typically require traditional server infrastructure rather than more cost-effective serverless or edge options.
Maintenance Challenges
1. Framework Updates: Custom servers require manual updates when upgrading Next.js versions, as they operate outside the standard upgrade path.
2. Divergence from Documentation: Most Next.js documentation assumes the standard server, making troubleshooting more difficult with custom implementations.
3. Knowledge Transfer: Custom server implementations create additional onboarding challenges for new team members who must understand both Next.js and your custom server logic.
Compatibility Issues
1. Feature Incompatibility: Many Next.js features may not work as expected with custom servers, including:
1. Incremental Static Regeneration (ISR)
2. On-demand Revalidation
3. Image Optimization API
4. Middleware (in some configurations)
2. Standalone Output Mode: Custom servers are incompatible with Next.js's standalone output mode, which is designed to optimize deployments.
3. Next.js Compiler: Custom server files don't run through the Next.js compiler, requiring manual compatibility with your Node.js version.
Security Considerations
1. Security Updates: Custom servers may miss security improvements automatically applied to the standard Next.js server.
2. Manual Security Implementation: Security features like CORS, rate limiting, and request validation must be manually implemented and maintained.
3. Increased Attack Surface: Custom servers potentially introduce additional security vulnerabilities if not correctly configured and maintained.
Scaling Challenges
1. Manual Scaling Logic: Custom scaling logic must be implemented rather than leveraging platform-provided scaling for standard Next.js applications.
2. Resource Utilization: Custom servers often have less efficient resource utilization than the optimized standard Next.js server.
3. Global Distribution Complexity: Implementing global distribution and edge presence becomes significantly more complex with custom servers.
Development Workflow Impacts
1. Development/Production Parity: Maintaining parity between development and production environments becomes more challenging.
2. Hot Module Replacement (HMR): Custom servers may interfere with Next.js's HMR capabilities, requiring manual configuration to maintain developer experience.
3. Debugging Complexity: Debugging becomes more complex as issues could stem from either Next.js or the custom server implementation.
Migration Difficulties
1. Lock-in Effect: Once implemented, migrating away from a custom server can be challenging as application logic becomes intertwined with server implementation.
2. Refactoring Overhead: Significant refactoring may be required to move from a custom server to standard Next.js patterns.
3. Technical Debt: Custom servers often become sources of technical debt as Next.js evolves with new features that aren't compatible with custom implementations.
Why you might not need a custom server and how to migrate away from it
The performance and maintainability implications mentioned above provide a good incentive not to use custom servers and migrate away from them if you've used them in your project.
Conveniently, Next.js has evolved a lot recently, and many of the use cases mentioned above that historically required custom servers can now be addressed using built-in Next.js features. Here's how modern Next.js handles these scenarios without custom servers:
1. Custom Routing → Dynamic Routes
Next.js now provides comprehensive routing capabilities through its file-system-based router, which includes dynamic segments, catch-all routes, and optional catch-all routes.
You can use the following patterns to achieve most of the use cases that historically required custom servers:
`
2. Request/Response Manipulation → Middleware
Next.js Middleware provides a standardized way to modify requests and responses before they reach your application.
You can easily implement custom headers, CORS, rate limiting, and more using middleware:
`
3. Authentication → Middleware + Auth Libraries
Next.js Middleware combined with authentication libraries like NextAuth.js provides a more maintainable and secure approach to authentication.
Instead of using a custom server, you can implement authentication logic in middleware like this:
`
4. WebSockets → Standalone WebSocket Server
To preserve Next.js optimizations, you can implement WebSockets without a custom server using a standalone WebSocket server approach. This is more compatible with modern deployment platforms and preserves Next.js optimizations.
To migrate away from a custom server, you can follow these steps:
1. Create a Standalone WebSocket Server
First, create a separate WebSocket server file:
`
2. Create a WebSocket Client Hook
`
3. Update Your Next.js Application
`
4. Configure Your Deployment
For production, you'll need to set up a proxy to forward WebSocket requests to your standalone server:
`
5. Update Your Package Scripts
`
This approach gives you the best of both worlds: Next.js's optimized rendering and routing with the real-time capabilities of WebSockets, all without sacrificing deployment options or performance.
5. Background Processing → unstable_after API
The new unstable_after API in Next.js 15 allows for background processing after a response has been sent.
Instead of using a custom server, you can use this API to execute code after a response has been sent:
`
Please note that this API is experimental and not yet stable, so it's important to watch the Next.js blog for updates.
6. Proxying Requests → Rewrites
Next.js config rewrites provide a declarative way to proxy requests without custom server code.
To proxy requests to an external API, you can use the following configuration, eliminating the need for a custom server:
`
Use Cases That May Still Require Custom Servers
While a lot of use cases that historically required custom servers can now be addressed by fully leveraging modern Next.js features, there are still some scenarios that may require a custom server implementation:
- Non-HTTP Protocol Support: When your application needs to support protocols beyond HTTP and WebSockets, you might still need a custom server.
- Deep Integration with Existing Systems: A custom server may be required for scenarios requiring tight integration with existing non-Node.js systems where the integration point must be at the server level.
- Highly Specialized Performance Requirements: A custom server may be needed for applications with extremely specific performance needs that can't be addressed through Next.js's built-in optimizations.
- Complex Multi-Application Architectures: When building complex architectures that don't fit the standard Next.js model and require custom orchestration at the server level, you might not be able to avoid a custom server.
What to do if you need a custom server
Follow best practices! That means if a custom server is absolutely necessary for your use case:
1. Minimize Custom Logic: Keep custom server logic to an absolute minimum.
2. Isolate Custom Code: Clearly separate custom server code from Next.js application code.
3. Document Thoroughly: Maintain detailed documentation explaining why the custom server is necessary and how it works.
4. Regular Reassessment: Periodically reassess whether the custom server is still necessary as Next.js evolves.
5. Monitoring: Implement comprehensive monitoring to quickly identify performance or stability issues related to the custom server.
Conclusion
Custom servers are a powerful tool that can address specific use cases that are not easily solved with modern Next.js features. However, they come with significant trade-offs in performance, deployment options, maintenance overhead, and compatibility with Next.js features.
Before implementing a custom server, thoroughly evaluate whether modern Next.js features like middleware, API routes, and rewrites can address your requirements without the drawbacks of a custom server implementation.
If you already have a custom server implemented, consider migrating to modern Next.js features if your use case can be addressed with them, as it will likely bring more benefits than drawbacks....