Hien Phan
Hien Phan
Strategy
BlogProjectsAbout
StrategyBlogProjectsAbout

Apps

🎯English📹YouTube🚀Indie⚡Learn⚙️Dashboard
Back to Writing

Advanced Next.js Patterns: Server Actions, Middleware, and More

Deep dive into advanced Next.js patterns including Server Actions, Edge Middleware, Suspense boundaries, and performance optimization.

January 24, 2026•8 min read•Boilerplate Team
Next.js
Next.js
Server Actions
Middleware
Performance
React 19
Advanced Next.js Patterns: Server Actions, Middleware, and More

Next.js has evolved significantly with the App Router. This article covers advanced patterns that will level up your Next.js applications.

Next.js 16 Updates

This guide covers Next.js 16 with React 19, including the latest Server Actions patterns and Edge Runtime improvements.

Server Actions: The Modern Way

Server Actions allow you to run server code directly from your components, replacing API routes for mutations.

Basic Server Action

// app/actions.ts

"use server";

import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";

export async function updateProfile(formData: FormData) {
  const userId = formData.get("userId") as string;
  const name = formData.get("name") as string;

  // Server-side validation
  if (!name || name.length < 2) {
    return { error: "Name must be at least 2 characters" };
  }

  // Database update
  await db.user.update({
    where: { id: userId },
    data: { name },
  });

  // Revalidate cache
  revalidatePath("/profile");

  return { success: true };
}
Best Practice

Group Server Actions in dedicated files (e.g., app/actions.ts, app/actions/users.ts). Use "use server" at the top of the file rather than individual functions.

Server Action with Zod Validation

import { z } from "zod";
import { revalidatePath } from "next/cache";

const updateProfileSchema = z.object({
  userId: z.string().min(1),
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Invalid email address"),
  bio: z.string().max(500, "Bio must be less than 500 characters").optional(),
});

export async function updateProfile(formData: FormData) {
  // Parse and validate
  const result = updateProfileSchema.safeParse({
    userId: formData.get("userId"),
    name: formData.get("name"),
    email: formData.get("email"),
    bio: formData.get("bio"),
  });

  if (!result.success) {
    return { error: result.error.flatten().fieldErrors };
  }

  const { userId, name, email, bio } = result.data;

  // Update database
  await db.user.update({
    where: { id: userId },
    data: { name, email, bio },
  });

  revalidatePath("/profile");
  revalidatePath("/settings");

  return { success: true, data: result.data };
}

Using Server Actions in Components

"use client";

import { useFormStatus } from "react-dom";
import { updateProfile } from "@/app/actions";

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Saving..." : "Save Changes"}
    </button>
  );
}

export function ProfileForm({ user }: { user: User }) {
  return (
    <form action={updateProfile}>
      <input name="userId" type="hidden" value={user.id} />
      <input name="name" defaultValue={user.name} />
      <input name="email" type="email" defaultValue={user.email} />
      <textarea name="bio" defaultValue={user.bio} />
      <SubmitButton />
    </form>
  );
}

Edge Middleware Patterns

Middleware runs on the Edge, making it perfect for auth, redirects, and geolocation.

Suspense Boundaries and Loading States

Suspense allows you to show loading UI while data fetches.

Parallel Data Fetching

// app/dashboard/page.tsx

import { Suspense } from "react";
import { StatsCardSkeleton } from "@/components/ui/loading-state";

async function UserStats() {
  const stats = await fetchUserStats(); // Suspends until complete
  return <StatsCards data={stats} />;
}

async function RecentActivity() {
  const activities = await fetchRecentActivity(); // Suspends until complete
  return <ActivityList items={activities} />;
}

export default function DashboardPage() {
  return (
    <div className="space-y-6">
      <h1>Dashboard</h1>

      {/* These fetch in parallel */}
      <Suspense fallback={<StatsCardSkeleton />}>
        <UserStats />
      </Suspense>

      <Suspense fallback={<div>Loading activity...</div>}>
        <RecentActivity />
      </Suspense>
    </div>
  );
}

Streaming with Suspense

// app/blog/[slug]/page.tsx

import { Suspense } from "react";
import { notFound } from "next/navigation";

async function BlogPost({ slug }: { slug: string }) {
  const post = await fetchBlogPost(slug); // Suspends

  if (!post) {
    notFound();
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <MDXRenderer content={post.content} />
    </article>
  );
}

async function RelatedPosts({ slug }: { slug: string }) {
  const posts = await fetchRelatedPosts(slug); // Fetches in parallel

  return (
    <aside>
      <h2>Related Posts</h2>
      <PostList posts={posts} />
    </aside>
  );
}

export default function BlogPage({ params }: { params: { slug: string } }) {
  return (
    <div className="grid lg:grid-cols-[1fr_300px] gap-8">
      <Suspense fallback={<div>Loading post...</div>}>
        <BlogPost slug={params.slug} />
      </Suspense>

      <Suspense fallback={<div>Loading related posts...</div>}>
        <RelatedPosts slug={params.slug} />
      </Suspense>
    </div>
  );
}

Loading UI Best Practices

PatternWhen to UseExample
Page-level loading.tsxSlow pages (2+ seconds)Dashboard, analytics
Inline SuspenseComponent-level loadingComments, recommendations
Skeleton screensContent with predictable layoutCards, lists, tables
SpinnerIndeterminate loadingFile uploads, processing

Performance Optimization

Dynamic Imports with Code Splitting

"use client";

import { useState, Suspense } from "react";
import dynamic from "next/dynamic";

// Dynamically import heavy components
const HeavyChart = dynamic(
  () => import("@/components/heavy-chart"),
  {
    loading: () => <div>Loading chart...</div>,
    ssr: false, // Skip SSR for client-only components
  }
);

const RichTextEditor = dynamic(
  () => import("@/components/rich-text-editor"),
  {
    loading: () => <div>Loading editor...</div>,
  }
);

export function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <button onClick={() => setShowChart(true)}>Show Analytics</button>

      {showChart && (
        <Suspense fallback={<div>Loading...</div>}>
          <HeavyChart />
        </Suspense>
      )}

      <RichTextEditor />
    </div>
  );
}

Image Optimization

import Image from "next/image";

export function ProductImage({ src, alt }: { src: string; alt: string }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}
      height={600}
      priority={false} // Set true for above-fold images
      placeholder="blur" // Shows blur placeholder while loading
      blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      className="rounded-lg"
    />
  );
}
Image Dimensions

Always provide width and height (or use fill with object-fit) to prevent layout shift. Use priority for above-fold images.

Caching Strategies

import { unstable_cache } from "next/cache";

// Cache expensive database queries
export const getUser = unstable_cache(
  async (id: string) => {
    return db.user.findUnique({ where: { id } });
  },
  ["user"],
  {
    revalidate: 3600, // Cache for 1 hour
    tags: ["users"], // Tag for manual revalidation
  }
);

// Fetch with caching options
async function fetchBlogPosts() {
  const res = await fetch("https://api.example.com/posts", {
    next: {
      revalidate: 3600, // Cache for 1 hour
      tags: ["blog-posts"], // Tag for revalidation
    },
  });

  return res.json();
}

// Manual cache revalidation
import { revalidateTag, revalidatePath } from "next/cache";

export async function revalidatePosts() {
  revalidateTag("blog-posts"); // Revalidate by tag
  revalidatePath("/blog"); // Revalidate by path
}

Error Handling

Error Boundaries

// app/error.tsx

"use client";

import { useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    console.error("Error:", error);
  }, [error]);

  return (
    <div className="flex items-center justify-center min-h-screen">
      <Card className="max-w-md">
        <CardHeader>
          <CardTitle>Something went wrong!</CardTitle>
        </CardHeader>
        <CardContent>
          <p className="text-muted-foreground mb-4">
            {error.message || "An unexpected error occurred"}
          </p>
          <Button onClick={reset}>Try again</Button>
        </CardContent>
      </Card>
    </div>
  );
}

Global Error Handler

// app/global-error.tsx

"use client";

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <html>
      <body>
        <div className="min-h-screen flex items-center justify-center">
          <div>
            <h2>Something went seriously wrong!</h2>
            <button onClick={reset}>Try again</button>
          </div>
        </div>
      </body>
    </html>
  );
}
Level Up Your Next.js Skills

These patterns will help you build faster, more scalable Next.js applications. Server Actions reduce boilerplate, Suspense improves UX, and proper caching ensures performance.

Quick Reference

Next

Getting Started with Next.js and shadcn/ui

Hien Phan
Hien Phan

Indie maker building in public. Ship fast, learn loud.

Navigation

  • Blog
  • Projects
  • About

Contact

harrisonphan5@gmail.com

Based in Vietnam 🇻🇳

© 2026 Hien Phan. All rights reserved.

Built withNext.js+Vercel