API Routes vs Server Actions
När ska du använda Route Handlers och när Server Actions? En beslutsguide med konkreta exempel på webhooks, formulär och externa konsumenter.
En av de vanligaste frågorna när man arbetar med Next.js App Router är var logiken ska leva: i en Route Handler eller i en Server Action? Svaret beror inte på hur komplex logiken är, utan på vem som anropar den och varifrån.
Den här guiden ger dig en tydlig mental modell, konkreta kodexempel och en beslutstabell du kan ta med in i nästa arkitekturdiskussion.
Vad är skillnaden?
Route Handlers (app/api/route.js) är HTTP-endpoints med en faktisk URL. De accepterar specifika HTTP-metoder (GET, POST, PUT och så vidare) och returnerar ett Response-objekt. Vilken HTTP-klient som helst kan nå dem: en mobilapp, en tredjepartstjänst, ett cron-jobb eller en webhook-avsändare.
Server Actions är funktioner märkta med 'use server'. De anropas direkt från React-komponenter via ett internt Next.js-mekanismsystem. De exponerar ingen stabil publik URL i vanlig mening. De är designade för mutationer som triggas av ditt eget gränssnitt.
Den mentala modellen:
- Route Handler: en API-endpoint du publicerar för världen utanför din Next.js-app.
- Server Action: en serverfunktion du anropar från ditt eget frontend.
Fråga dig själv: skulle du klistra in URL:en i ett externt dashboards webhook-fält, eller skulle du importera funktionen i en React-komponent? Det räcker oftast som svar.
När du ska använda Route Handlers
1. Webhooks från externa tjänster
Stripe skickar ett POST-anrop till en URL när en betalning genomförs. Stripe känner inte till dina React-komponenter. Det behöver en riktig HTTP-endpoint.
// app/api/webhooks/stripe/route.js
export async function POST(request) {
const body = await request.text()
const signatur = request.headers.get('stripe-signature')
const handelse = stripe.webhooks.constructEvent(
body,
signatur,
process.env.STRIPE_WEBHOOK_SECRET
)
if (handelse.type === 'payment_intent.succeeded') {
await registreraBetalning(handelse.data.object)
}
return Response.json({ mottagen: true })
}
Samma mönster gäller för GitHub-webhooks, Svea Ekonomi-callbacks, Klarna-notifieringar och alla andra externa tjänster som kommunicerar via HTTP.
2. Mobilapp eller extern klient hämtar data
Om en React Native-app eller ett annat webbfrontend behöver hämta data från din Next.js-backend behöver det en URL att anropa. En Server Action kan inte anropas utifrån Next.js-appens gränser.
// app/api/produkter/route.js
export async function GET(request) {
const { searchParams } = new URL(request.url)
const kategori = searchParams.get('kategori')
const produkter = await hamtaProdukter({ kategori })
return Response.json(produkter)
}
Mobilappen anropar /api/produkter?kategori=jackor och får JSON tillbaka. Enkelt och fristående.
3. Publik REST API
Om du exponerar ett API för tredjepartsintegrationer är Route Handlers det enda alternativet. Server Actions är kopplade till Next.js interna transportlager och är inte tänkta att dokumenteras och konsumeras externt.
4. Cron-jobb från externa tjänster
Tjänster som Vercel Cron, GitHub Actions eller en extern schemaläggare skickar HTTP-anrop för att trigga jobb. De behöver en URL:
// app/api/cron/daglig-rapport/route.js
export async function GET(request) {
const token = request.headers.get('authorization')
if (token !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Obehörig', { status: 401 })
}
await genereraDagligRapport()
return Response.json({ klar: true })
}
När du ska använda Server Actions
1. Formulärsubmission som skriver till databasen
Ett kontaktformulär som sparar meddelanden. Formuläret finns i din Next.js-app, och åtgärden körs på servern. Ingen separat API-rutt behövs.
// app/kontakt/actions.js
'use server'
import { revalidatePath } from 'next/cache'
export async function sparaMeddelande(formData) {
const namn = formData.get('namn')
const meddelande = formData.get('meddelande')
await db.meddelande.create({ data: { namn, meddelande } })
revalidatePath('/admin/meddelanden')
}
// app/kontakt/page.jsx
import { sparaMeddelande } from './actions'
export default function KontaktSida() {
return (
<form action={sparaMeddelande}>
<input name="namn" required />
<textarea name="meddelande" required />
<button type="submit">Skicka</button>
</form>
)
}
React hanterar formulärinlämningen och anropar Server Action direkt. Inget fetch, inget API-anrop, inget extra lager.
2. Databasmutation från ett admin-gränssnitt
Att radera ett inlägg, publicera en artikel eller uppdatera en användares roll är alla operationer som triggas från ditt eget gränssnitt. Server Actions passar perfekt här.
// app/admin/actions.js
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function raderaInlagg(inlaggId) {
await db.inlagg.delete({ where: { id: inlaggId } })
revalidatePath('/admin/inlagg')
redirect('/admin/inlagg')
}
export async function publiceraArtikel(artikelId) {
await db.artikel.update({
where: { id: artikelId },
data: { publicerad: true, publiceradDatum: new Date() },
})
revalidatePath('/artiklar')
}
3. Cacheinvalidering efter mutation
revalidatePath och revalidateTag fungerar tekniskt sett i båda, men de är naturligast inuti Server Actions. Mutationen och cacheinvalideringen sker i samma funktion, vilket gör flödet lättare att följa och felsöka.
'use server'
import { revalidateTag } from 'next/cache'
export async function uppdateraProdukt(produktId, data) {
await db.produkt.update({ where: { id: produktId }, data })
revalidateTag(`produkt-${produktId}`)
revalidateTag('produktlista')
}
4. Optimistisk UI-uppdatering
Server Actions integrerar med React useOptimistic-hooken, vilket gör det enkelt att visa en omedelbar respons i gränssnittet medan servermutationen slutförs i bakgrunden. Det är ett mönster som är specifikt designat för Server Actions och inte har en naturlig motsvarighet med Route Handlers.
Beslutstabell
| Scenario | Använd |
|---|---|
| Webhook från extern tjänst (Stripe, GitHub) | Route Handler |
| Mobilapp hämtar data | Route Handler |
| Formulärsubmission från din UI | Server Action |
| Databasmutation triggas från komponent | Server Action |
| Publik REST API | Route Handler |
| Cacheinvalidering efter mutation | Server Action |
| Auth-callback (OAuth redirect) | Route Handler |
| Optimistisk UI-uppdatering | Server Action |
| Cron-jobb från extern tjänst | Route Handler |
Hybridfall
Vissa scenarier kan fungera med antingen det ena eller det andra. Det som avgör är hur mycket kontroll du behöver och vem som triggar operationen.
Filuppladdning
Server Actions hanterar filuppladdningar via FormData och fungerar bra för enkla fall: en profilbild, ett bifogat dokument. Det kräver minimalt med kod och passar naturligt in i ett formulärflöde.
Route Handlers ger mer kontroll för stora filer: streaming, anpassad progress-rapportering eller direktuppladdning till S3 med presigned URL. För de flesta appar är Server Action-varianten enklare och tillräcklig. Välj Route Handler när du faktiskt behöver det extra lagret.
Auth-callbacks
OAuth-leverantörer omdirigerar till en URL efter autentisering, till exempel /api/auth/callback/github. Det är alltid en Route Handler, för OAuth-leverantören gör HTTP-anropet, inte ditt eget frontend. Leverantören vet bara om URL:en du registrerade.
Bakgrundsjobb triggas från UI
Om en användare klickar "starta jobb" och du behöver sätta igång en långvarig process kan en Server Action trigga den. Men om en extern schemaläggare också behöver trigga samma jobb på ett cron-schema behöver det jobbet även en Route Handler. I det fallet är det praktiskt att extrahera jobblogiken till en delad funktion och anropa den från båda ställena:
// lib/jobb/processaOrder.js
export async function processaOrder(orderId) {
// Affärslogiken lever här, fristående från transportlagret
}
// app/api/cron/ordrar/route.js
import { processaOrder } from '@/lib/jobb/processaOrder'
export async function POST(request) {
const { orderId } = await request.json()
await processaOrder(orderId)
return Response.json({ klar: true })
}
// app/ordrar/actions.js
'use server'
import { processaOrder } from '@/lib/jobb/processaOrder'
export async function startaOrderProcessning(orderId) {
await processaOrder(orderId)
}
Logiken dupliceras inte. Transportlagret väljs beroende på vem som anropar.
Vanliga misstag
Server Action i en extern integration. Det är frestande att återanvända en Server Action för ett webhook-scenario, men det fungerar inte. Server Actions exponeras inte på ett sätt som är stabilt eller dokumenterat för externa anropare. Lägg webhook-hanteringen i en Route Handler.
Route Handler för alla formulär. Många utvecklare som kommer från Pages Router-tänket lägger automatiskt formulärlogik i app/api/.... Det är onödigt komplexitet när Server Actions ger ett renare flöde: ingen fetch, ingen JSON-serialisering, ingen extra felhantering på klienten.
Cacheinvalidering i Route Handler för data som renderas med React. Om du invaliderar revalidatePath i en Route Handler fungerar det, men det är ett tecken på att operationen egentligen borde vara en Server Action. Håll mutations- och cacheinvalideringslogiken nära varandra.
Rekommendation
Defaulta till Server Actions för allt som triggas från ditt eget Next.js-gränssnitt: formulär, mutationer, cacheinvalidering. Använd Route Handlers när något utanför din Next.js-app behöver göra HTTP-anropet.
Det praktiska testet: om du skulle klistra in URL:en i ett tredjepartsdashboard (Stripe, GitHub, en cron-tjänst) är det en Route Handler. Om du skulle import-era funktionen i en React-komponent är det en Server Action.
De två mekanismerna är inte konkurrenter utan komplement. En väldesignad Next.js-app använder båda, var för sig, för det de är bra på.
