Next.js and Custom Servers

·

6 min read

Introduction:

Next.js has become a popular choice for building full-stack applications, offering both client-side and server-side capabilities. However, as projects grow in complexity, developers often find themselves needing a separate custom server to handle specific tasks. In this comprehensive guide, we'll explore the pros and cons of using Next.js alongside a custom server, discuss authentication challenges, and provide practical examples to help you create a robust full-stack architecture.

Table of Contents:

  1. Next.js: The Good and the Bad

  2. When to Use a Custom Server

  3. Architecture Overview

  4. Authentication Challenges

  5. Implementing Authentication with Kinde

  6. Project Structure and Examples

  7. Bonus: Project Ideas and Tips

  8. Next.js: The Good and the Bad

Next.js is a powerful full-stack framework that provides both client-side and server-side capabilities. Let's break down its components:

Client-side:

  • Client components

Server-side:

  • Server components

  • Server actions

  • Route handlers (API routes)

Pros of Next.js:

  • Unified framework for both frontend and backend

  • Easy server-side rendering (SSR) and static site generation (SSG)

  • Built-in API routes

  • Seamless deployment to platforms like Vercel

Cons of Next.js:

  • Limited by serverless function constraints

  • Potential cold start issues

  • No persistent file system in serverless environments

  • Limited flexibility for complex backend tasks

  1. When to Use a Custom Server

While Next.js covers many use cases, there are scenarios where a custom server becomes necessary:

a) Long-running tasks: Tasks that exceed serverless function time limits (e.g., video rendering, web scraping)

b) Background jobs: Cron jobs, job queues, and other scheduled tasks

c) Database connection management: Better control over connection pools

d) Persistent file system: When you need to work with local files or SQLite

e) WebSocket support: Real-time communication that may not work out-of-the-box with Next.js

f) Specialized technologies: Using languages or frameworks optimized for specific tasks (e.g., Python for machine learning)

  1. Architecture Overview

A typical architecture combining Next.js with a custom server might look like this:

+-------------------+      +-------------------+
|                   |      |                   |
|   Next.js App     |      |   Custom Server   |
|                   |      |                   |
| +---------------+ |      | +---------------+ |
| |  Client-side  | |      | |   API Routes  | |
| |  Components   | |      | |               | |
| +---------------+ |      | +---------------+ |
|                   |      |                   |
| +---------------+ |      | +---------------+ |
| | Server-side   | |      | | Long-running | |
| | Components    | |      | |    Tasks     | |
| +---------------+ |      | +---------------+ |
|                   |      |                   |
| +---------------+ |      | +---------------+ |
| | API Routes    | |      | | Background   | |
| |               | |      | |    Jobs      | |
| +---------------+ |      | +---------------+ |
|                   |      |                   |
+-------------------+      +-------------------+

This setup allows you to leverage the strengths of both Next.js and a custom server, creating a more flexible and scalable architecture.

  1. Authentication Challenges

One of the biggest challenges when combining Next.js with a custom server is implementing a cohesive authentication system. You need to ensure that:

a) Users can authenticate in the Next.js application

b) Authenticated requests from Next.js server-side to the custom server are possible

c) Direct client-side requests to the custom server are authenticated

d) The custom server can verify tokens issued by the Next.js authentication system

  1. Implementing Authentication with Kinde

To solve these authentication challenges, we'll use Kinde, a powerful authentication solution that works well with Next.js and custom servers. Here's a step-by-step guide:

Step 1: Set up Kinde in your Next.js application

// Install the Kinde SDK
npm install @kinde-oss/kinde-auth-nextjs

// Set up environment variables in .env.local
KINDE_CLIENT_ID=your_client_id
KINDE_CLIENT_SECRET=your_client_secret
KINDE_ISSUER_URL=https://your_subdomain.kinde.com

// Create a middleware.ts file to protect routes
import { withAuth } from "@kinde-oss/kinde-auth-nextjs/middleware";

export default withAuth({
  protectedPaths: ["/dashboard"],
});

export const config = {
  matcher: ["/dashboard"],
};

Step 2: Implement authentication in your Next.js components

// pages/index.js
import { LoginLink, RegisterLink } from "@kinde-oss/kinde-auth-nextjs/components";

export default function Home() {
  return (
    <div>
      <h1>Welcome to Stock Prices</h1>
      <LoginLink>Log in</LoginLink>
      <RegisterLink>Sign up</RegisterLink>
    </div>
  );
}

// pages/dashboard.js
import { useKindeAuth } from "@kinde-oss/kinde-auth-nextjs/client";
import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server";

export default function Dashboard() {
  const { user, isLoading } = useKindeAuth();

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h1>Welcome, {user.given_name}!</h1>
      {/* Dashboard content */}
    </div>
  );
}

export async function getServerSideProps(context) {
  const { getToken } = getKindeServerSession(context);
  const token = await getToken();

  // Use token to make authenticated requests to your custom server
  const res = await fetch("http://localhost:3001/api/protected", {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  const data = await res.json();

  return {
    props: { data },
  };
}

Step 3: Set up authentication in your custom server

// custom-server/server.js
const express = require("express");
const { expressjwt: jwt } = require("express-jwt");
const jwks = require("jwks-rsa");

const app = express();

const jwtCheck = jwt({
  secret: jwks.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: "https://your_subdomain.kinde.com/.well-known/jwks.json",
  }),
  audience: "http://localhost:3001",
  issuer: "https://your_subdomain.kinde.com",
  algorithms: ["RS256"],
});

app.use(jwtCheck);

app.get("/api/protected", (req, res) => {
  res.json({ message: "This is protected stock prices data" });
});

app.listen(3001, () => {
  console.log("Custom server running on port 3001");
});
  1. Project Structure and Examples

Here's a sample project structure for a Next.js app with a custom server:

my-fullstack-app/
├── next-app/
│   ├── pages/
│   ├── components/
│   ├── public/
│   ├── styles/
│   ├── lib/
│   ├── middleware.ts
│   └── package.json
├── custom-server/
│   ├── src/
│   │   ├── routes/
│   │   ├── controllers/
│   │   ├── models/
│   │   └── services/
│   ├── server.js
│   └── package.json
├── shared/
│   ├── types/
│   └── utils/
└── package.json
  1. Bonus: Project Ideas and Tips

To help you get started with this architecture, here are some project ideas:

  1. Real-time stock trading platform:

    • Next.js for the frontend and real-time updates

    • Custom server for handling complex trading algorithms and WebSocket connections

  2. Video editing web application:

    • Next.js for the user interface and project management

    • Custom server for video processing and rendering tasks

  3. AI-powered content recommendation system:

    • Next.js for the user-facing application

    • Custom Python server for machine learning models and data processing

Tips for success:

  • Use TypeScript in both Next.js and your custom server for better type safety

  • Implement proper error handling and logging in both applications

  • Consider using a message queue (e.g., RabbitMQ, Redis) for communication between Next.js and the custom server

  • Implement rate limiting and caching strategies to optimize performance

  • Use environment variables for configuration management across both applications

Conclusion:

Combining Next.js with a custom server can provide the best of both worlds: a powerful, easy-to-use frontend framework with the flexibility of a custom backend. By following the architecture and authentication patterns outlined in this guide, you can create robust, scalable full-stack applications that meet the demands of complex projects.

Remember to always consider your specific project requirements when deciding on your architecture, and don't be afraid to adapt and evolve your setup as your application grows.

Did you find this article valuable?

Support Mikey's Blog by becoming a sponsor. Any amount is appreciated!