Back to blog
tutorial

10 Practical Next.js Examples for Real-World Apps

Explore 10 practical Next.js examples for building modern apps. See code for forms, auth, and e-commerce, with tips for integrating FormBackend.

J
Jesper Christiansen

You’ve probably done this before. You start with a clean Next.js app, wire up a couple of pages, and then the actual application’s demands become apparent. A contact form needs spam protection. A checkout flow needs server-side logic. A blog needs preview mode. An onboarding flow needs draft persistence because users abandon halfway through.

That’s where most “next js examples” stop being useful. They show the happy path, but real projects need patterns you can ship. Next.js has become a mainstream production framework, with over 4.5 million weekly downloads reported by Prismic in 2026, which helps explain why so many example projects map closely to real production work rather than toy demos (Prismic on Next.js example projects).

This guide stays practical. These are 10 Next.js examples worth studying when you need working patterns for landing pages, commerce, CMS-driven sites, dashboards, real-time features, and multi-step forms. Forms get special attention throughout because they tend to spread backend concerns across validation, notifications, redirects, uploads, and storage. For many teams, that’s where a lightweight service like FormBackend fits cleanly into a Next.js stack without adding another custom API surface.

Table of Contents

1. Next.js Official Examples Repository

The official examples are still the best place to calibrate your instincts. They’re not useful because they’re flashy. They’re useful because they show how the framework authors expect routing, data loading, handlers, and deployment assumptions to fit together.

Vercel’s official showcase describes Next.js as being used across thousands of websites and positions it around high-performance, server-rendered applications rather than static toy projects (Next.js showcase). That’s the reason official examples matter. They’re often the shortest path from docs-level understanding to production structure.

Why it still matters

If you’re new to Next.js, start with examples that solve one problem at a time. Authentication, route handlers, image handling, and deployment all look simple until you mix them.

A good habit is to clone one example and then add one real feature from your project instead of stacking three examples on day one.

  • Start narrow: Pick a sample that matches your actual app shape, such as content, dashboard, or commerce.
  • Preserve folder intent: Don’t immediately reorganize everything. Learn why the structure works before “cleaning it up.”
  • Lift patterns, not whole apps: Keep the data flow, error boundaries, and route conventions. Replace the business logic.

What to copy and what to ignore

Copy the boring parts first. Request parsing, route placement, environment handling, and server/client boundaries matter more than visual polish.

Ignore any sample code that exists only to demonstrate a single API in isolation. A lot of developers copy demo state management or placeholder fetch logic and then wonder why the app turns brittle.

Practical rule: If an example doesn’t survive your auth, validation, and deployment constraints, it’s reference material, not architecture.

2. T3 Stack

T3 is the example I reach for when a project needs strong TypeScript pressure from the start. It helps when your app has forms that feed database writes, authenticated actions, and admin views that all need to agree on shape.

The upside is obvious. You get fewer “frontend says one thing, backend expects another” moments. The downside is also obvious. You inherit more architecture on day one.

Where T3 shines

T3 fits internal tools, SaaS dashboards, and products with complex relational data. If a form creates a parent record, attaches child records, and then triggers follow-up UI state, type-safe boundaries pay off quickly.

A simple shape might look like this:

// server/api/routers/project.ts
import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "../trpc";

export const projectRouter = createTRPCRouter({
  create: protectedProcedure
    .input(
      z.object({
        name: z.string().min(1),
        description: z.string().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      return ctx.db.project.create({
        data: {
          ...input,
          ownerId: ctx.session.user.id,
        },
      });
    }),
});

That style works well when the form schema, server validation, and database write all need to stay aligned.

A good fit and a bad fit

T3 is a good fit when your team already wants TypeScript-first conventions. It’s a bad fit for a marketing site with one contact form and a tiny admin surface.

  • Use it for dense apps: Dashboards, account areas, admin panels, and workflow-heavy products.
  • Skip it for brochure sites: You don’t need a full typed backend pattern for a two-page launch site.
  • Add optional pieces slowly: Don’t turn on every integration because the starter offers it.

The common mistake is choosing T3 for speed and then spending that saved time learning stack-specific abstractions you didn’t need.

3. Next.js + Tailwind CSS Landing Page Template

A landing page template looks easy until you need it to convert. That means responsive layout, clean SEO defaults, fast image handling, and a form that doesn’t break when someone uses it.

This pattern works especially well because Next.js keeps routing and deployment straightforward, while Tailwind makes it cheap to iterate on layout without introducing a custom design system too early.

A visual reference helps here:

A hand-drawn style web design illustration showing a computer, smartphone, and a contact form with SEO elements.

The pattern that works

For landing pages, I’d keep most of the page server-rendered or statically rendered, then isolate the form interaction inside a small client component. That keeps the page fast and avoids pushing all content rendering into the browser.

A simple structure:

// app/page.tsx
import Hero from "@/components/hero";
import ContactForm from "@/components/contact-form";

export default function HomePage() {
  return (
    <main className="mx-auto max-w-6xl px-6 py-16">
      <Hero />
      <section className="mt-20">
        <h2 className="text-2xl font-semibold">Talk to us</h2>
        <ContactForm />
      </section>
    </main>
  );
}

A simple contact form

For many landing pages, you don’t need a custom route handler at all. You need reliable submission handling, spam filtering, redirects, and notifications. That’s the kind of form logic that often belongs outside your app code.

"use client";

import { useState } from "react";

export default function ContactForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "done" | "error">("idle");

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus("sending");

    const formData = new FormData(e.currentTarget);

    const res = await fetch(e.currentTarget.action, {
      method: "POST",
      body: formData,
    });

    setStatus(res.ok ? "done" : "error");
  }

  return (
    <form
      action="https://www.formbackend.com/f/your-form-endpoint"
      method="POST"
      onSubmit={onSubmit}
      className="mt-6 space-y-4"
    >
      <input name="name" placeholder="Name" className="w-full rounded border p-3" />
      <input name="email" type="email" placeholder="Email" className="w-full rounded border p-3" />
      <textarea name="message" placeholder="Message" className="w-full rounded border p-3" />
      <button className="rounded bg-black px-5 py-3 text-white">Send</button>
      {status === "done" && <p>Thanks. Your message was sent.</p>}
    </form>
  );
}

That approach keeps your landing page lean. You’re not building and maintaining a backend route just to email yourself a lead.

4. Next.js E-Commerce Starter

Commerce examples are where Next.js starts showing its real value. Product catalogs, collections, search pages, carts, and checkout flows rarely want the same rendering strategy.

Independent case-study guidance around Next.js highlights a practical split: product pages can use Incremental Static Regeneration with on-demand revalidation, while authenticated areas and checkout typically use server-side rendering so personalized and sensitive data stays fresh at request time (commerce-oriented Next.js rendering patterns).

A hand-drawn e-commerce illustration showing a canvas backpack product page, shopping cart, and order confirmation receipt.

Use mixed rendering on purpose

This is the mistake I see most often in e-commerce starters. Teams pick one rendering mode and force the whole app into it. That usually means either overusing SSR for pages that could be prebuilt, or overusing static generation for pages that depend on per-user state.

A better split looks like this:

  • Use ISR for catalog pages: Product detail pages, category pages, and editorial merchandising pages.
  • Use SSR for account state: Cart review, checkout, addresses, and order history.
  • Revalidate intentionally: Trigger content refresh when product data changes instead of rebuilding everything.

A sketch of a product route:

// app/products/[slug]/page.tsx
export const revalidate = 300;

export default async function ProductPage({ params }: { params: { slug: string } }) {
  const product = await getProduct(params.slug);

  return (
    <main>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </main>
  );
}

Forms around the checkout matter too

Commerce apps don’t just have payment forms. They also have restock alerts, warranty registration, quote requests, and post-purchase support flows.

Those forms are easy to neglect because the checkout gets all the attention. If you need confirmation emails without building another mail pipeline, FormBackend’s guide on sending notification email after form submissions is a practical fit for those supporting workflows.

Product pages should be cheap to serve. Checkout should be correct. Don’t optimize those two concerns the same way.

5. Next.js Blog Starter

Blog starters remain some of the best Next.js examples because they compress several core framework ideas into one small app. Routing, pre-rendering, dynamic paths, metadata, and content fetching all show up fast.

Capgemini’s example-based guide explains the older page-based routing model clearly, including how files in the pages directory map directly to routes and how dynamic routes like pages/articles/[aid]/[comment].tsx pair with getStaticProps and getStaticPaths for static generation and dynamic path handling (Capgemini on Next.js fundamentals through examples). Even if you’re building with App Router today, that mental model still helps.

Why blog examples are so teachable

A blog gives you just enough complexity to learn without drowning in infrastructure. You have lists, detail pages, slugs, content transforms, and often a preview workflow.

A page-based example looks like this:

// pages/posts/[slug].tsx
export async function getStaticPaths() {
  const posts = await getAllPosts();
  return {
    paths: posts.map((post) => ({ params: { slug: post.slug } })),
    fallback: false,
  };
}

export async function getStaticProps({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);
  return { props: { post } };
}

export default function PostPage({ post }: { post: { title: string; body: string } }) {
  return <article><h1>{post.title}</h1><div>{post.body}</div></article>;
}

A useful blog feedback flow

Most blog starters stop at publishing. Real blogs often need lightweight feedback, correction requests, or article-specific contact forms.

  • Keep comments scoped: Attach hidden fields like slug or article title so you know what the message refers to.
  • Don’t overbuild moderation first: Start with private submissions before adding a public comment system.
  • Use forms for editorial signals: “Was this helpful?” and “Report an issue” are often more useful than open comments.

That’s where a simple form endpoint can carry more value than a full community feature you won’t maintain.

6. Next.js SaaS Boilerplate

A SaaS boilerplate earns its keep when it reduces repeated decisions. Authentication, protected layouts, account settings, billing pages, and team membership logic all create drag if you keep reinventing them.

The temptation is to choose the biggest starter possible. That usually backfires. Heavy boilerplates can lock you into assumptions about tenancy, permissions, or billing that don’t match your product.

Good boilerplates reduce drift

A good SaaS starter gives you stable boundaries. Public marketing routes stay separate from app routes. Protected server logic doesn’t leak into random components. Settings pages use the same action and validation patterns throughout.

That consistency matters more than feature count.

// app/(app)/settings/page.tsx
import { getCurrentUser } from "@/lib/auth";
import SettingsForm from "@/components/settings-form";

export default async function SettingsPage() {
  const user = await getCurrentUser();
  return <SettingsForm user={user} />;
}

Where teams usually overbuild

They overbuild forms inside authenticated areas. Profile updates, invite forms, support requests, cancellation flows, beta requests, and sales handoff forms all end up scattered across custom handlers.

Instead, separate core product writes from peripheral form workflows. Product-critical mutations should live in your application backend. Supportive workflows can often be handled with simpler infrastructure. This is also where user-facing polish matters. Form abandonment usually comes from friction, not schema complexity, and FormBackend’s article on form user experience is a useful reference when you’re tightening those flows.

7. Next.js Portfolio Personal Website Example

A portfolio site sounds tiny, but it still has production concerns. You need fast page loads, image-heavy sections, project pages that rank well, and an inquiry form that doesn’t become a spam magnet.

This is one of the best places to keep things intentionally boring. A personal site benefits from simple architecture more than clever abstractions.

Small site, real constraints

Use static rendering for the main pages unless the site requires request-time data. Keep your project entries structured, not hardcoded into one giant JSX file, so you can add case studies later without rewriting the layout.

A clean project card setup:

type Project = {
  slug: string;
  title: string;
  summary: string;
};

export function ProjectCard({ project }: { project: Project }) {
  return (
    <a href={`/projects/${project.slug}`} className="block rounded border p-5">
      <h3 className="text-lg font-semibold">{project.title}</h3>
      <p className="mt-2 text-sm text-gray-600">{project.summary}</p>
    </a>
  );
}

Inquiry forms should route cleanly

For freelance and agency-style portfolios, the inquiry form is the actual business endpoint. Treat it like one.

  • Ask for routing fields: Budget range, project type, timeline, and website URL help triage leads.
  • Send users to a real success state: Don’t leave them staring at the same form wondering if it worked.
  • Keep uploads optional: File fields are useful for briefs, but don’t force them upfront.

A portfolio isn’t a gallery. It’s a filtered lead intake system with a design layer on top.

8. Next.js API-Driven Application

API-driven apps are where your rendering choices, cache behavior, and error handling become visible. Pulling content from a CMS or another backend is easy in the demo stage. It gets harder when preview mode, localized content, and partial failures enter the picture.

This category also overlaps with commerce. If your project mixes editorial and catalog data, it’s worth studying adjacent ideas from headless commerce solutions because the same separation-of-concerns problems show up quickly.

External data changes how you structure pages

The page file shouldn’t know everything about the external service. Move fetching logic into a thin data layer so you can centralize retries, auth headers, and transforms.

// lib/cms.ts
export async function getPageBySlug(slug: string) {
  const res = await fetch(`${process.env.CMS_URL}/pages/${slug}`, {
    headers: { Authorization: `Bearer ${process.env.CMS_TOKEN}` },
    next: { revalidate: 300 },
  });

  if (!res.ok) throw new Error("Failed to fetch page");
  return res.json();
}

Then consume it in a server component and keep the JSX focused on rendering.

What App Router examples often miss

A lot of App Router examples explain the happy path but skip the boundary between Server Components and Client Components. That causes confusion fast when developers pass components through props or move logic to dynamic imports.

One recurring gap in beginner-focused Next.js examples is exactly that server/client boundary, especially around preserving server execution when a server component is rendered inside a client component and around how ssr: false changes execution behavior (discussion of common App Router misunderstandings).

  • Keep data fetching server-side by default: Move to client components only when the browser needs to own the interaction.
  • Be careful with dynamic import flags: ssr: false isn’t a harmless optimization toggle.
  • Pass rendered trees when useful: Sometimes children preserve better boundaries than serializing data down into a client island.

9. Next.js with Real-Time Data

Real-time features are compelling, but they’re also the fastest way to complicate a clean app. Before you add sockets, make sure the user benefits from seeing state change immediately.

This pattern shines for chats, collaborative tools, live ops dashboards, and event streams. It’s usually unnecessary for basic CRUD.

Where real-time belongs

If a stale view creates confusion or duplicate work, real-time likely helps. If users can tolerate a refresh or background revalidation, don’t reach for sockets first.

A common pattern is to render the page normally, then connect a client-only channel for updates:

"use client";

import { useEffect, useState } from "react";

export default function LiveNotifications() {
  const [messages, setMessages] = useState<string[]>([]);

  useEffect(() => {
    const socket = new WebSocket("wss://example.com/live");
    socket.onmessage = (event) => {
      setMessages((prev) => [event.data, ...prev]);
    };
    return () => socket.close();
  }, []);

  return (
    <ul>
      {messages.map((msg, i) => <li key={i}>{msg}</li>)}
    </ul>
  );
}

Here’s a walkthrough format many developers find useful when wiring the pattern end to end:

Keep the first page load simple

Don’t make WebSocket connection logic responsible for the initial page render. The first render should work without a live channel, then subscribe for freshness.

That split makes testing easier and failure modes much cleaner. If the socket drops, the page still works.

10. Next.js Multi-Step Form Wizard Pattern

Multi-step forms, a common time sink for many teams, seem straightforward until you need validation per step, back/next navigation, branching, recoverable drafts, mobile usability, and a final submission payload that doesn’t lose context.

A wizard is one of the best next js examples to study because it forces you to design state deliberately instead of stuffing everything into uncontrolled inputs and hoping the user reaches the final screen.

A hand-drawn sketch of a multi-step user onboarding interface showing account plan details and a success notification.

State first, submission second

The right approach is to treat the wizard as an application state problem before it becomes a networking problem. Keep a single canonical object for the full draft, validate each step independently, and persist between refreshes if the flow is long enough.

"use client";

import { useState } from "react";

type WizardData = {
  accountType?: string;
  companyName?: string;
  email?: string;
};

export default function SignupWizard() {
  const [step, setStep] = useState(1);
  const [data, setData] = useState<WizardData>({});

  function update(values: Partial<WizardData>) {
    setData((prev) => ({ ...prev, ...values }));
  }

  return (
    <div>
      {step === 1 && (
        <div>
          <button onClick={() => { update({ accountType: "team" }); setStep(2); }}>
            Team plan
          </button>
        </div>
      )}
      {step === 2 && (
        <input
          placeholder="Company name"
          onChange={(e) => update({ companyName: e.target.value })}
        />
      )}
    </div>
  );
}

A production-ready wizard submission

The final submit step should be boring. By then, all branching and validation should already be resolved.

For a serverless submission path, FormBackend has a dedicated Next.js form setup guide that fits well when you want the wizard UI in your app but don’t want to stand up custom form-processing infrastructure for the final handoff.

  • Validate per step: Users should see errors where they happen, not on the confirmation screen.
  • Persist drafts carefully: Local storage is fine for non-sensitive draft recovery.
  • Branch explicitly: Don’t hide conditions in random JSX checks. Keep them in one flow map or reducer.

Multi-step forms fail when state shape is accidental. The UI just reveals that problem.

10 Next.js Examples: Side-by-Side Comparison

Example Implementation complexity Resource requirements Expected outcomes Ideal use cases Key advantages
Next.js Official Examples Repository Varies (simple to advanced) Code samples, occasional external services Ready-to-use reference projects and patterns Learning, prototyping, architecture reference Authoritative, regularly updated, well-documented
T3 Stack (Next.js + tRPC + Prisma + TypeScript) Moderate to high TypeScript tooling, Prisma DB, tRPC, Node environment End-to-end type-safe full-stack applications Type-safe apps, rapid production scaffolding Strong type safety, excellent DX, preconfigured tooling
Next.js + Tailwind CSS Landing Page Template Low Tailwind, Next.js build, minimal backend for forms Fast, responsive, SEO-friendly landing pages SaaS/product launches, agency sites, lead gen High performance, quick customization, SEO-ready
Next.js E-Commerce Starter (Shopify/Stripe) High Payment provider accounts, database, webhook infra Production-ready shopping, checkout, and order flows Online stores, subscriptions, marketplaces Complete transaction flow, payment security patterns
Next.js Blog Starter (Markdown/CMS) Moderate Headless CMS or Markdown pipeline, build infra SEO-optimized content site with SSG/ISR Blogs, docs, content-heavy sites Fast static performance, flexible content sources
Next.js SaaS Boilerplate (Auth + DB) High Auth services, database, billing (Stripe), multi-tenant infra Scalable SaaS features: dashboards, billing, RBAC SaaS products, team collaboration platforms Accelerates SaaS development, built-in security patterns
Next.js Portfolio / Personal Website Example Low Static content, minimal backend for contact forms Lightweight portfolio with contact and project showcases Freelancers, designers, personal brands Quick to personalize and deploy, performance-focused
Next.js API-Driven Application (Headless CMS) Moderate to high Headless CMS accounts, API calls, caching strategy Decoupled content-driven app with preview and caching Enterprise sites, multi-site networks, localized content Clear content/presentation separation, strong caching options
Next.js with Real-Time Data (Socket.io/WebSockets) High WebSocket servers, connection management, scaling infra Live updates, notifications, collaborative features Live dashboards, chat, collaborative editors True real-time UX, supports collaborative workflows
Next.js Multi-Step Form / Wizard Pattern Moderate Client state management, persistence (localStorage), backend for submission Progressive multi-step forms with higher completion rates Onboarding, complex registrations, applications Improves completion, reduces user cognitive load

Your Next.js Blueprint for Faster Shipping

The best Next.js examples aren’t the ones with the most features. They’re the ones that teach durable decisions. Routing that stays understandable six months later. Rendering choices that match the page’s job. Form handling that doesn’t sprawl into three custom endpoints and an inbox rule nobody remembers.

That’s why these ten patterns are useful together. The official examples teach framework defaults. T3 shows what stronger end-to-end typing looks like. Landing pages and portfolios show how little infrastructure you can get away with when you keep scope tight. Commerce, SaaS, and API-driven builds show where complexity enters and which parts need stronger boundaries. Real-time apps and multi-step wizards force you to be honest about state, synchronization, and user failure modes.

Security deserves a final note too. A lot of example code treats remote images, redirects, and data fetching as harmless glue. That’s not always true. Independent security research has shown that image-related misconfiguration in Next.js apps can create SSRF and XSS risk, especially around allowlisted image domains, redirects, SVG handling, and older framework versions (Assetnote research on SSRF in Next.js apps). If you copy example code into production, review every external fetch, redirect, image config, and upload path like production code, because that’s what it becomes.

The practical takeaway is simple. Start from an example that matches your app shape, not the trendiest stack on social media. Keep server components server-side unless the browser needs ownership. Use mixed rendering where the product demands it. Treat forms as workflows, not just HTML tags.

If you want to ship full stack applications fast, Next.js gives you a strong foundation, but speed comes from reducing unnecessary custom infrastructure. That’s where a form service can fit naturally. FormBackend is one option when your project needs submission handling, notifications, redirects, or workflow automation without building all of that into your own Next.js backend.

Pick one example from this list that resembles your next project. Build the smallest real feature first. Then keep the parts that still feel boring after launch. Those are usually the right ones.

Add a form backend to your site in minutes

Connect any HTML form to FormBackend and start collecting submissions — no backend code required.

Start free