ScreenshotNeo

BlogGuides

Next.js: A Developer Guide

Build and ship a modern Next.js app with the App Router, covering rendering, data, security, SEO, performance, and production release.

By the ScreenshotNeo team1 October 20269 min read

Next.js is a React framework for building full-stack web applications. For a new project, learn the App Router first. It is file-system based and uses React Server Components, Suspense, and Server Functions. The Pages Router remains supported, so existing applications can continue using it while you migrate at your own pace.

This guide shows how to create an application, choose server and client boundaries, fetch data, control freshness, stream slow work, secure mutations, add metadata, and verify a production build.

1. Create a Next.js application

The official quick start is create-next-app. Check the current installation documentation for the requirements and defaults that match your installed version. The current canary installation guide lists Node.js 20.9 or newer and supports macOS, Windows including WSL, and Linux. These requirements can change between releases.

npx create-next-app@latest my-next-app
cd my-next-app
npm run dev

Open http://localhost:3000. During setup, choose TypeScript, ESLint, and the App Router unless your project has a specific reason to use the Pages Router. The official installation guide documents each prompt and default: Next.js installation.

2. Understand the App Router structure

Routes live under app. A folder becomes a URL segment, page.tsx renders the route, and layout.tsx wraps child routes.

app/
  layout.tsx          # Root HTML and shared UI
  page.tsx            # /
  about/page.tsx      # /about
  products/[id]/page.tsx  # /products/:id
  loading.tsx         # Loading UI for this segment
  error.tsx           # Error boundary for this segment
  not-found.tsx       # 404 UI for this segment
  api/health/route.ts # Route Handler
public/               # Static files

A minimal root layout and page:

// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Acme products',
  description: 'Browse Acme products',
}

export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang='en'>
      <body>{children}</body>
    </html>
  )
}

// app/page.tsx
export default function HomePage() {
  return <main><h1>Products</h1></main>
}

The Pages Router uses pages/index.tsx, pages/[id].tsx, and data functions such as getServerSideProps. It is still supported in newer Next.js versions; use the Pages Router documentation when maintaining that architecture.

3. Server Components and Client Components

App Router components are Server Components by default. They render on the server and do not require JavaScript in the browser for their static output. Add 'use client' at the top of a file when that component needs state, event handlers, effects, or browser APIs.

// app/counter.tsx
'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)
  return (
    <button onClick={() => setCount(value => value + 1)}>
      Count: {count}
    </button>
  )
}

Keep the client boundary close to the interactive part. A Server Component can render a Client Component and pass serializable props to it. Do not put database clients, private tokens, or server-only modules in a Client Component.

4. Fetch data and decide how fresh it must be

Server Components can use asynchronous fetch calls or an ORM/database client. Identical fetch requests in a component tree are memoized by default. Fetch requests are not universally cached by default, so choose caching and request-time rendering deliberately for the Next.js version and deployment you use.

// app/products/page.tsx
import { Suspense } from 'react'

async function ProductList() {
  const response = await fetch('https://api.example.com/products', {
    // Choose an explicit policy for your application:
    next: { revalidate: 60 },
  })
  if (!response.ok) throw new Error('Could not load products')
  const products: { id: string; name: string }[] = await response.json()

  return (
    <ul>
      {products.map(product => <li key={product.id}>{product.name}</li>)}
    </ul>
  )
}

export default function ProductsPage() {
  return (
    <main>
      <h1>Products</h1>
      <Suspense fallback={<p>Loading products…</p>}>
        <ProductList />
      </Suspense>
    </main>
  )
}

Use a request-time result when users must see current data. Use a cache or revalidation when reuse is more valuable than immediate freshness. Request-time APIs such as cookies, headers, or user-specific data can opt a route into dynamic rendering. Verify the behavior of your chosen data source and deployment rather than relying on a blanket rule.

5. Stream slow sections with Suspense

A slow upstream service remains slow; streaming changes when the response appears. Put a boundary near the slow work so the rest of the page can render first.

// app/dashboard/page.tsx
import { Suspense } from 'react'

async function Revenue() {
  const response = await fetch('https://api.example.com/revenue', {
    cache: 'no-store',
  })
  if (!response.ok) throw new Error('Revenue request failed')
  const data: { total: number } = await response.json()
  return <p>Revenue: ${data.total}</p>
}

export default function DashboardPage() {
  return (
    <main>
      <h1>Dashboard</h1>
      <p>This heading can render immediately.</p>
      <Suspense fallback={<p>Loading revenue…</p>}>
        <Revenue />
      </Suspense>
    </main>
  )
}

You can also add app/dashboard/loading.tsx for a segment-level loading state. Make fallbacks meaningful and accessible; avoid replacing the whole page with an indefinite spinner when only one panel is waiting.

6. Handle mutations and security

Authentication and authorization are application responsibilities. Check authorization inside every Server Action, not only in a layout, page, or proxy. Keep database access in a server-only data layer and consider rate limits for expensive operations.

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function createNote(formData: FormData) {
  const session = await getSession() // Implement with your auth provider
  if (!session?.userId) throw new Error('Unauthorized')

  const text = String(formData.get('text') ?? '').trim()
  if (!text || text.length > 5000) throw new Error('Invalid note')

  await database.note.create({
    data: { text, userId: session.userId },
  })
  revalidatePath('/notes')
}

// app/notes/new/page.tsx
import { createNote } from '../actions'

export default function NewNotePage() {
  return (
    <form action={createNote}>
      <label htmlFor='text'>Note</label>
      <textarea id='text' name='text' required />
      <button type='submit'>Save</button>
    </form>
  )
}

Store secrets in environment files that are ignored by Git. Only variables intentionally exposed to the browser should use the NEXT_PUBLIC_ prefix. Validate input on the server even when the form also validates it in the browser.

7. Add metadata, accessibility, and error states

The Metadata API defines titles and descriptions. Add Open Graph images, a sitemap, and a robots file as appropriate; these mechanisms do not guarantee search rankings.

// app/about/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'About Acme',
  description: 'Learn how Acme builds its products.',
  openGraph: {
    title: 'About Acme',
    description: 'Learn how Acme builds its products.',
    images: ['/og/about.png'],
  },
}

export default function AboutPage() {
  return <main><h1>About Acme</h1></main>
}

Use semantic headings, labels, keyboard-accessible controls, useful focus states, and descriptive alternative text for meaningful images. Add error.tsx and not-found.tsx where users need recovery actions.

8. Test a production-like build

Development mode does not represent production behavior. Run the production build and start commands before release.

npm run build
npm run start

Review route and error handling, authentication and authorization, request-time APIs, caching, streaming, accessibility, metadata, type safety, Core Web Vitals, and bundle size. Confirm that environment variables exist in the deployment environment and that database migrations run safely.

9. Common problems and fixes

Symptom Likely cause Fix
Hooks or event handlers fail in a page The file is a Server Component Add 'use client' to the smallest interactive component.
A secret appears in browser code The module crossed a client boundary or used NEXT_PUBLIC_ Move the operation to a Server Component or Server Action and keep the variable private.
Data is unexpectedly stale A cache or revalidation policy is serving reused data Inspect the request options and invalidation path; use request-time rendering when freshness is required.
Every request is slow Independent work is blocking one render Fetch independent data in parallel and place Suspense boundaries around slow sections.
Dynamic APIs change rendering behavior Cookies, headers, or user-specific data opt the route into dynamic rendering Use those APIs intentionally and document the freshness and caching requirement.
Server Action returns unauthorized Authorization is missing, expired, or checked only in a proxy Validate the session and resource permissions inside the action itself.
404 or error UI never appears The special file is in the wrong route segment Place not-found.tsx or error.tsx beside the segment it handles.
Production differs from development Only development mode was checked Run next build followed by next start with production environment variables.

10. Performance, reliability, and cost decisions

  • Rendering: Keep server work on the server, but use client components where interaction requires them. Smaller client boundaries reduce browser JavaScript.
  • Data: Cache reusable results, revalidate when acceptable, and use request-time rendering for personalized or rapidly changing data.
  • Streaming: Stream slow portions with meaningful fallbacks; this improves perceived progress without accelerating the upstream service.
  • Reliability: Handle failed fetches, timeouts, empty states, retries where safe, and idempotent mutations. Monitor errors and route health after release.
  • Cost: Database calls, third-party APIs, bandwidth, and server execution are deployment concerns. Measure your workload and set limits for expensive operations.

11. Capture your Next.js pages

To create visual regression fixtures or documentation screenshots, you can automate a browser yourself with Playwright or another browser runner. Wait for the page state your test needs, set the viewport, and save the image. Browser automation gives control but requires managing browsers, fonts, consent banners, popups, retries, and failed navigations.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the verdict and billing status.

See the full option list and parameter reference in the ScreenshotNeo documentation. This cURL example captures a page:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

For Next.js documentation and QA workflows, options include full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets or custom viewports, retina scale, custom CSS and JavaScript, click actions, selector waits, delays, network-idle waits, blocked ads and trackers, custom headers and cookies, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, PDF page settings, usage reporting, and an OpenAPI specification. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

There is a free allowance of 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is included on every plan. Create a free ScreenshotNeo account.

12. Next.js FAQ

Should a new project use the App Router?

Use the App Router when learning current Next.js features or starting a new application. Keep the Pages Router when its conventions fit an existing codebase; it remains supported.

Are all fetch requests cached?

No. Identical requests are memoized in a component tree, while caching and revalidation depend on the request and framework behavior. Choose and verify a policy explicitly.

Does streaming make an API faster?

No. It lets users receive completed parts while slower work continues.

Where should authorization run?

Inside each Server Action or server-side data operation that protects a resource. Do not rely only on a layout, page, or proxy check.

What command confirms production behavior?

Run next build and then next start with production configuration.