Node.jsBackendFull-Stack

Next.js Introduction for Node.js Developers

TT
TopicTrick Team
Next.js Introduction for Node.js Developers

Next.js Introduction for Node.js Developers

If you have built APIs with Express and applications with React, Next.js is the natural next step. It unifies both into a single framework with conventions that eliminate the boilerplate of wiring them together — routing, server rendering, data fetching, and API endpoints all live in the same codebase with a consistent file system structure.

This module introduces Next.js through the lens of what you already know as a Node.js developer. You will understand what Next.js adds, when to use it, and how to get a full-stack application running quickly.

This is Module 23 of the Node.js Full‑Stack Developer course.


Creating a Next.js App

bash
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
cd my-app
npm run dev

Visit http://localhost:3000. The app is running on a Node.js server managed by Next.js.


App Router File Conventions

text
app/
├── layout.js          ← Root layout (wraps every page)
├── page.js            ← Route: /
├── about/
│   └── page.js        ← Route: /about
├── blog/
│   ├── page.js        ← Route: /blog
│   └── [slug]/
│       └── page.js    ← Route: /blog/:slug (dynamic)
├── dashboard/
│   ├── layout.js      ← Nested layout for /dashboard/*
│   └── page.js        ← Route: /dashboard
└── api/
    └── posts/
        └── route.js   ← API endpoint: GET/POST /api/posts

Every page.js is a React component. Every route.js is an API handler. Folder names define the URL structure.


React Server Components (Default)

By default, every component in the App Router is a Server Component — it renders on the server and sends HTML to the browser:

jsx
// app/blog/page.js — Server Component
// No 'use client' directive = server-only

import { prisma } from '@/lib/prisma';

// This function runs on the server — direct DB access, no API call needed
export default async function BlogPage() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
    take: 10,
    include: { author: { select: { name: true } } },
  });

  return (
    <main>
      <h1>Blog</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <a href={`/blog/${post.id}`}>{post.title}</a>
            <span> by {post.author.name}</span>
          </li>
        ))}
      </ul>
    </main>
  );
}

No useEffect. No fetch('/api/posts'). No loading state for the initial render. The component awaits the database directly and returns HTML.


Client Components

For interactivity (event handlers, state, browser APIs), mark a component with 'use client':

jsx
// components/LikeButton.jsx — Client Component
'use client';

import { useState } from 'react';

export function LikeButton({ postId, initialCount }) {
  const [count, setCount] = useState(initialCount);
  const [liked, setLiked] = useState(false);

  async function handleLike() {
    setCount(c => c + (liked ? -1 : 1));
    setLiked(l => !l);
    await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
  }

  return (
    <button onClick={handleLike}>
      {liked ? '❤️' : '🤍'} {count}
    </button>
  );
}
jsx
// app/blog/[slug]/page.js — Server Component that uses a Client Component
import { LikeButton } from '@/components/LikeButton';

export default async function PostPage({ params }) {
  const post = await prisma.post.findUnique({ where: { id: params.slug } });

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
      {/* LikeButton is a Client Component — it hydrates in the browser */}
      <LikeButton postId={post.id} initialCount={post.likes} />
    </article>
  );
}

Server Components can render Client Components. Client Components cannot render Server Components (they can only import them as children passed via props).


API Routes (route.js)

Next.js API routes are HTTP handlers in route.js files — similar to Express routes but without the framework overhead:

js
// app/api/posts/route.js
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';

// GET /api/posts
export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const page  = parseInt(searchParams.get('page') ?? '1');
  const limit = parseInt(searchParams.get('limit') ?? '20');

  const posts = await prisma.post.findMany({
    where: { published: true },
    orderBy: { createdAt: 'desc' },
    skip: (page - 1) * limit,
    take: limit,
  });

  return NextResponse.json({ posts });
}

// POST /api/posts
export async function POST(request) {
  const body = await request.json();

  const post = await prisma.post.create({
    data: { title: body.title, body: body.body, authorId: body.authorId },
  });

  return NextResponse.json(post, { status: 201 });
}
js
// app/api/posts/[id]/route.js
export async function GET(request, { params }) {
  const post = await prisma.post.findUnique({ where: { id: params.id } });
  if (!post) return NextResponse.json({ error: 'Not found' }, { status: 404 });
  return NextResponse.json(post);
}

export async function PUT(request, { params }) {
  const body = await request.json();
  const post = await prisma.post.update({ where: { id: params.id }, data: body });
  return NextResponse.json(post);
}

export async function DELETE(request, { params }) {
  await prisma.post.delete({ where: { id: params.id } });
  return new Response(null, { status: 204 });
}

Export a function named after the HTTP method — GET, POST, PUT, DELETE, PATCH. Next.js routes them automatically.


Middleware (middleware.js)

Next.js middleware runs before every request — on the Edge, before the server:

js
// middleware.js (root of project)
import { NextResponse } from 'next/server';
import { verifyAccessToken } from '@/lib/tokens';

export function middleware(request) {
  const { pathname } = request.nextUrl;

  // Protect /dashboard/* routes
  if (pathname.startsWith('/dashboard')) {
    const token = request.headers.get('Authorization')?.replace('Bearer ', '');

    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }

    try {
      verifyAccessToken(token);
    } catch {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};

Server Actions

Server Actions are the Next.js way to handle form submissions and mutations without a separate API route:

jsx
// app/posts/new/page.js
import { redirect } from 'next/navigation';
import { prisma } from '@/lib/prisma';

async function createPost(formData) {
  'use server';                    // ← makes this a Server Action

  const title = formData.get('title');
  const body  = formData.get('body');

  if (!title || !body) throw new Error('Title and body are required');

  const post = await prisma.post.create({
    data: { title, body, authorId: 'current-user-id' },
  });

  redirect(`/blog/${post.id}`);
}

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input  name="title" placeholder="Title" required />
      <textarea name="body" placeholder="Content" required />
      <button type="submit">Publish</button>
    </form>
  );
}

The form submits directly to the Server Action — no fetch, no API route, no event handler.


Data Fetching Patterns

jsx
// Static data — fetched once at build time
export const dynamic = 'force-static';

export default async function StaticPage() {
  const data = await fetch('https://api.example.com/data', {
    cache: 'force-cache',   // default — cached indefinitely
  }).then(r => r.json());
  return <div>{data.title}</div>;
}

// Dynamic data — fetched on every request
export const dynamic = 'force-dynamic';

export default async function DynamicPage() {
  const data = await fetch('https://api.example.com/data', {
    cache: 'no-store',     // never cache
  }).then(r => r.json());
  return <div>{data.title}</div>;
}

// Revalidated data — cached, refreshed every N seconds (ISR)
export default async function RevalidatedPage() {
  const data = await fetch('https://api.example.com/data', {
    next: { revalidate: 60 },   // refresh every 60 seconds
  }).then(r => r.json());
  return <div>{data.title}</div>;
}

Layouts

Layouts wrap pages and persist across navigations — ideal for navigation bars, sidebars, and auth shells:

jsx
// app/layout.js — Root layout
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <nav>/* global nav */</nav>
        <main>{children}</main>
        <footer>/* global footer */</footer>
      </body>
    </html>
  );
}

// app/dashboard/layout.js — Nested layout for /dashboard/*
export default function DashboardLayout({ children }) {
  return (
    <div className="flex">
      <aside>/* sidebar */</aside>
      <section>{children}</section>
    </div>
  );
}

Next.js vs Express: When to Use Which

ConsiderationNext.jsExpress
Full-stack with React✅ IdealManual wiring
Pure REST/GraphQL APIOverkill✅ Ideal
Multiple client typesComplex✅ Ideal
SSR / SEO✅ Built-inManual setup
File-system routing✅ Built-inManual
Streaming / RSC✅ Built-inNot available
Deployment flexibilityVercel-optimisedAny platform
Team knows ReactSeparate concern

Node.js Full‑Stack Course — Module 23 of 32

You understand where Next.js fits in the Node.js ecosystem. Continue to Module 24 to build a full-stack Task Manager project applying everything from this course.


    Summary

    Next.js is Node.js with React conventions and production defaults baked in:

    • The App Router uses file-system routing — folders and page.js files define URLs
    • Server Components run on the server, access databases directly, and ship zero JavaScript — use them by default
    • Client Components ('use client') handle interactivity — keep them small and at the leaves of the component tree
    • API Routes (route.js) export named HTTP method functions — GET, POST, etc. — no Express needed
    • Server Actions ('use server') handle form submissions and mutations without a separate API endpoint
    • Middleware runs on the Edge before every request — ideal for auth guards and redirects
    • Choose Next.js for full-stack React apps; choose Express for pure APIs consumed by multiple clients

    Continue to Module 24: Full-Stack Task Manager Project →