Next.js 15 fullstack (App Router + Server Actions) · leksjon

Modulgrenser med server-only og client-only

Hindre at serverhemmeligheter og tung kode lekker inn i klientpakker ved hjelp av poison-pill-pakker.

Leksjon 3 av 413 trinn

Modulgrenser med server-only og client-only er en gratis leksjon i Next.js 15 fullstack (App Router + Server Actions) på CoddyKit. Dette er leksjon 3 av 4. Du kan lese valgfritt 3 leksjoner fra denne læringsstien gratis i sin helhet – deretter låser CoddyKit PRO opp alle leksjoner, samt praktisk øving med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i Next.js 15 fullstack (App Router + Server Actions), og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i Next.js 15 fullstack (App Router + Server Actions) inneholder totalt 4 leksjoner.

Hvorfor modulgrenser er viktige

Next.js 15 kjører JavaScript i to forskjellige miljøer: Node.js-serveren og nettleserklienten. Kode du skriver i app/, kan ubemerket ende opp i en av de to bundlene, avhengig av hvordan den importeres.

Uten eksplisitte grenser kan én uforsiktig importkjede eksponere:

  • databasepåloggingsdetaljer og API-nøkler som er lagret i miljøvariabler
  • serverlogikk som bare skal kjøres på serveren og aldri nå brukerne
  • tunge Node.js-biblioteker (crypto, fs, net) som gjør klientbundlen større

Sperrepakkene server-only og client-only er den idiomatiske Next.js-løsningen. De utløser en byggetidsfeil i det øyeblikket en modul krysser feil grense, slik at feilen oppdages før kode sendes til produksjon.

Installere pakkene

Begge pakkene publiseres av Next.js-teamet og inneholder ingen kjøretidskode. Hele formålet deres er å fungere som en vaktpostimport som bundleren gjenkjenner.

Installer dem én gang i prosjektet:

npm install server-only client-only

De er svært små – hver pakke inneholder bare én index.js som kaster en feil hvis den evalueres i feil miljø. Byggeprosessen (via Reacts bundlerbetingelser) sørger for at feilen utløses ved kompilering, ikke under kjøring.

Du trenger ikke å konfigurere noe i next.config.ts. Pakkene bygger på eksportbetingelsen react-server, som Next.js angir automatisk.

Markere en modul som serveronly

Alle filer som leser hemmeligheter på serversiden, kommuniserer med en database eller bruker innebygde Node.js-moduler, bør starte med én enkelt import:

import 'server-only'

Hvis en klientkomponent (eller annen klientkode) noen gang importerer denne modulen, avbryter Next.js bygget med en tydelig feilmelding som peker på importen som forårsaket problemet. Eksempelet nedenfor viser et datatilgangslag som aldri må nå nettleseren.

// lib/db.ts
import 'server-only'
import { Pool } from 'pg'

const pool = new Pool({
  connectionString: process.env.DATABASE_URL, // secret — server only
})

export async function getUserById(id: string) {
  const { rows } = await pool.query(
    'SELECT id, name, email FROM users WHERE id = $1',
    [id]
  )
  return rows[0] ?? null
}

Hva skjer når grensen brytes

Anta at en utvikler ved en feil importerer datatilgangslaget som bare skal brukes på serveren, inn i en klientkomponent. Uten sperrepakken ville hemmeligheten DATABASE_URL ubemerket ha dukket opp i nettleserbundlen.

Med server-only på plass stopper Next.js-bygget umiddelbart og skriver ut:

Error: This module cannot be imported from a Client Component module.
It should only be used from a Server Component.

Dette gjør det umulig å sende feilen til produksjon ved et uhell. Feilen er deterministisk – den utløses ved hver next build og ved hver HMR-omlasting med next dev som krysser grensen.

// app/dashboard/page.tsx — Server Component, safe to import lib/db
import { getUserById } from '@/lib/db'

export default async function DashboardPage() {
  const user = await getUserById('user_123')
  return <h1>Welcome, {user?.name}</h1>
}

// app/components/ProfileCard.tsx — Client Component
'use client'
// import { getUserById } from '@/lib/db'  // ← BUILD ERROR if uncommented
export function ProfileCard({ name }: { name: string }) {
  return <p>{name}</p>
}

Markere en modul som klientonly

client-only løser det motsatte problemet. Noen moduler er avhengige av API-er som bare finnes i nettleseren, for eksempel window, document, localStorage eller tredjeparts-SDK-er som er spesifikke for nettlesere.

Hvis en slik modul importeres i en serverkomponent, vil Node.js utløse en feil under kjøring fordi disse globale nettleserobjektene ikke finnes på serveren. client-only gjør denne uventede kjøretidsfeilen om til en byggetidsfeil.

Legg importen øverst i alle verktøyfiler som bare skal brukes i nettleseren:

import 'client-only'

// lib/analytics.ts
import 'client-only'

// This module calls browser APIs — it must never run on the server
export function trackEvent(name: string, props?: Record<string, unknown>) {
  if (typeof window === 'undefined') return // extra guard, but poison-pill fires first
  window.gtag?.('event', name, props)
}

export function getStoredUserId(): string | null {
  return localStorage.getItem('userId')
}

Trygt mønster: Serverdata sendt som props

Det kanoniske mønsteret i Next.js 15 er å hente data i en serverkomponent ved hjelp av en server-only-modul og deretter sende trygge, serialiserbare verdier videre til klientkomponenter som props.

Ingen hemmeligheter, databasehåndtak eller Node.js-spesifikke objekter krysser nettverksgrensen – bare vanlige data som trygt kan serialiseres til JSON.

// lib/user-service.ts
import 'server-only'
import { pool } from './db'

export interface UserProfile {
  id: string
  name: string
  avatarUrl: string
}

export async function getProfile(userId: string): Promise<UserProfile | null> {
  const { rows } = await pool.query(
    'SELECT id, name, avatar_url FROM users WHERE id = $1',
    [userId]
  )
  if (!rows[0]) return null
  return { id: rows[0].id, name: rows[0].name, avatarUrl: rows[0].avatar_url }
}

// app/profile/page.tsx — Server Component
import { getProfile } from '@/lib/user-service'
import { AvatarCard } from '@/components/AvatarCard' // 'use client'

export default async function ProfilePage({ params }: { params: { id: string } }) {
  const profile = await getProfile(params.id)
  if (!profile) return <p>Not found</p>
  // Only serialisable data crosses to the client
  return <AvatarCard name={profile.name} avatarUrl={profile.avatarUrl} />
}

Server Actions er ingen omvei

Server Actions er funksjoner som kjører på serveren, men kalles fra klientkode. En vanlig misforståelse er at det å merke en funksjon med 'use server' automatisk beskytter hele modulen.

Det gjør det ikke. Direktivet 'use server' forteller bare Next.js at funksjonen skal eksponeres som et HTTP-endepunkt. Andre eksporter i samme fil kan fortsatt lekke hvis de importeres direkte.

Beste praksis er å holde Server Actions i egne actions/-filer og fortsatt beskytte delte serververktøy med server-only.

// app/actions/update-profile.ts
'use server'
import { getUserById } from '@/lib/db' // lib/db has 'server-only' — safe
import { revalidatePath } from 'next/cache'

export async function updateProfileAction(formData: FormData) {
  const name = formData.get('name') as string
  const userId = formData.get('userId') as string

  // Business logic runs entirely on the server
  const existing = await getUserById(userId)
  if (!existing) throw new Error('User not found')

  // db update omitted for brevity
  revalidatePath('/profile')
}

// app/profile/edit/page.tsx — this is a Client Component form
'use client'
import { updateProfileAction } from '@/app/actions/update-profile'

export function EditForm({ userId }: { userId: string }) {
  return (
    <form action={updateProfileAction}>
      <input type="hidden" name="userId" value={userId} />
      <input name="name" placeholder="New name" />
      <button type="submit">Save</button>
    </form>
  )
}

Organisere lib/-mappen etter grense

En forutsigbar mappestruktur gjør det unødvendig å gjette hvilket miljø et verktøy er beregnet for. En utbredt struktur ser slik ut:

  • lib/server/ – hver fil starter med import 'server-only'
  • lib/client/ – hver fil starter med import 'client-only'
  • lib/shared/ – rene funksjoner uten miljøspesifikke importer (trygge i begge miljøer)

Med denne strukturen kan en kodegjennomgår umiddelbart se om et nytt verktøy er plassert riktig, før innholdet i det hele tatt leses. Regler for linting i CI kan også håndheve at filer i lib/server/ inneholder vaktpostimporten.

// lib/shared/format.ts — no sentinel needed, works anywhere
export function formatCurrency(amount: number, currency = 'USD'): string {
  return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)
}

// lib/server/stripe.ts
import 'server-only'
import Stripe from 'stripe'

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-11-20.acacia',
})

// lib/client/toast.ts
import 'client-only'
import { toast } from 'sonner'

export function showSuccess(msg: string) {
  toast.success(msg)
}

Kombinere med TypeScript-stialiaser

Stialiaser for TypeScript i tsconfig.json gjør grensebevisste importer praktiske og konsekvente i hele kodebasen. Du kan bygge grensen direkte inn i aliasnavnet, slik at importene dokumenterer seg selv.

// tsconfig.json (relevant excerpt)
// {
//   "compilerOptions": {
//     "paths": {
//       "@server/*": ["./lib/server/*"],
//       "@client/*": ["./lib/client/*"],
//       "@shared/*": ["./lib/shared/*"]
//     }
//   }
// }

// Usage in a Server Component:
import { stripe } from '@server/stripe'
import { formatCurrency } from '@shared/format'

// Usage in a Client Component:
import { showSuccess } from '@client/toast'
import { formatCurrency } from '@shared/format'

// Attempting to import @server/* in a Client Component
// triggers the server-only build error immediately.

Tredjepartspakker uten direktiver

Mange npm-pakker (særlig eldre pakker) inneholder ikke direktivet 'use client' eller 'use server'. Next.js-bundleren bruker en heuristikk: Hvis en pakke ikke har et direktiv, behandles den som delt og kan brukes i begge miljøer.

Dette kan være et problem når en pakke bruker globale nettleserobjekter internt. Løsningen er å pakke den inn i din egen lib/client/-modul, merket med client-only, slik at importkjeden er eksplisitt og etterprøvbar.

// lib/client/chart-wrapper.ts
import 'client-only'
// chart.js has no 'use client' directive but uses window internally
import { Chart, registerables } from 'chart.js'

Chart.register(...registerables)

export { Chart }
export type { ChartConfiguration } from 'chart.js'

// components/RevenueChart.tsx
'use client'
import { Chart } from '@client/chart-wrapper' // boundary enforced
import { useEffect, useRef } from 'react'

export function RevenueChart({ data }: { data: number[] }) {
  const ref = useRef<HTMLCanvasElement>(null)
  useEffect(() => {
    if (!ref.current) return
    const ctx = ref.current.getContext('2d')!
    new Chart(ctx, { type: 'line', data: { labels: data.map(String), datasets: [{ data }] } })
  }, [data])
  return <canvas ref={ref} />
}

Kontrollere bundlene med @next/bundle-analyzer

Sperrepakker hindrer utilsiktet lekkasje under bygging, men det er fortsatt nyttig å kontrollere bundlene visuelt. Pakken @next/bundle-analyzer genererer et interaktivt trekart over hver modul i hver bundle.

Bruk den til å bekrefte at servermoduler (databaseklienter, Stripe-SDK-er og tunge Node.js-biblioteker) ikke finnes i klientchunken, og omvendt. Hvis en modul som inneholder en hemmelighet, dukker opp i klientbundlen til tross for vaktpostimporten, betyr det vanligvis at en dynamisk import eller en re-eksport omgåtte kontrollen.

// next.config.ts
import type { NextConfig } from 'next'
import withBundleAnalyzer from '@next/bundle-analyzer'

const withAnalyzer = withBundleAnalyzer({
  enabled: process.env.ANALYZE === 'true',
})

const nextConfig: NextConfig = {
  // your existing config
}

export default withAnalyzer(nextConfig)

// Run analysis:
// ANALYZE=true next build
// Opens two browser tabs:
//   - client.html  (browser bundle — check for leaked server deps)
//   - server.html  (server bundle)

Kunnskapstest: server-only kontra client-only

Velg det beste svaret på spørsmålet nedenfor.

Oppsummering: Modulgrenser med server-only og client-only

I denne leksjonen lærte du hvordan du håndhever tydelige modulgrenser i en Next.js 15-applikasjon:

  • import 'server-only' – gjør modulen til en byggetidsfeil hvis den importeres fra klientkode. Bruk dette på databaseklienter, verktøy som leser hemmeligheter, og kode som bare fungerer i Node.js.
  • import 'client-only' – speiler mønsteret for moduler som bare fungerer i nettleseren, og oppdager import fra serversiden før det fører til krasj under kjøring.
  • Det trygge dataflytmønsteret: hent data i en serverkomponent ved hjelp av server-only-verktøy, og send deretter serialiserbare props til klientkomponenter – ingen hemmeligheter eller Node-API-er krysser nettverket.
  • Organiser lib/ i underkatalogene server/, client/ og shared/, og tilpass TypeScript-stialiasene slik at grensene dokumenterer seg selv.
  • Pakk inn tredjepartspakker som mangler direktiver, i egne moduler med markerte grenser, slik at importgrafen forblir etterprøvbar.
  • Kombiner sperrepakker med @next/bundle-analyzer for visuelt å kontrollere at ingen serveravhengigheter finnes i klientchunker.

Disse to pakkene koster nesten ingenting å legge til og eliminerer en hel klasse sikkerhets- og ytelsesfeil før de noen gang når produksjon.

Gratis å komme i gang

Lær deg TypeScript med en AI-veileder – gratis

Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.

Kurs
22
Leksjoner
88

Ofte stilte spørsmål

Er leksjonen «Modulgrenser med server-only og client-only» gratis?

Ja – du kan lese valgfritt 3 av leksjonene i læringsstien Next.js 15 fullstack (App Router + Server Actions), inkludert «Modulgrenser med server-only og client-only», gratis i sin helhet her på nettet. Deretter låser CoddyKit PRO opp alle leksjoner, samt interaktiv øving med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Kurset i Next.js 15 fullstack (App Router + Server Actions) inneholder totalt 4 leksjoner.

Hva lærer jeg i «Modulgrenser med server-only og client-only»?

Hindre at serverhemmeligheter og tung kode lekker inn i klientpakker ved hjelp av poison-pill-pakker. Du øver på Next.js 15 fullstack (App Router + Server Actions) med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.

Trenger jeg erfaring for å begynne med Next.js 15 fullstack (App Router + Server Actions)?

Ingen tidligere erfaring er nødvendig. Next.js 15 fullstack (App Router + Server Actions) på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 3 av 4.

Hvor lang tid tar leksjonen «Modulgrenser med server-only og client-only»?

De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.

Kan jeg skrive og kjøre kode i denne Next.js 15 fullstack (App Router + Server Actions)-leksjonen?

Ja. Alle Next.js 15 fullstack (App Router + Server Actions)-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.

Alle leksjonene i dette kurset

  1. Analysere og krympe klientpakken
  2. Grundig gjennomgang av Turbopack- og kompilatorkonfigurasjon
  3. Modulgrenser med server-only og client-only
  4. Dynamiske importer, kodedeling og lazy hydration
← Tilbake til Next.js 15 fullstack (App Router + Server Actions)