Stripe i Next.js: Komplett guide til betalingsintegrasjon
Implementer Stripe-betalinger i Next.js på riktig måte. Lær checkout, webhooks, abonnementer og norske betalingsløsninger.
Stripe i Next.js: Komplett guide til betalingsintegrasjon
Betalinger er det som gjør en hobby-app til en forretning. Men betalingsintegrasjon kan virke skremmende — sikkerhet, PCI-compliance, webhooks, og edge cases overalt.
Stripe gjør dette enkelt. Og med riktig oppsett i Next.js, kan du ha fungerende betalinger på under en time.
Denne guiden tar deg gjennom alt: fra enkel checkout til abonnementer, webhooks og norske betalingsløsninger.
Hvorfor Stripe for norske utviklere?
Stripe er den foretrukne betalingsløsningen for moderne webapplikasjoner:
| Egenskap | Stripe |
|---|---|
| Transaksjonsgebyr (Norge/EU) | 2,4% + 2 NOK |
| Internasjonale kort | 3,25% + 2 NOK |
| Ingen faste kostnader | ✓ |
| Abonnementshåndtering | Innebygd |
| Utviklerverktøy | Fremragende |
| Dokumentasjon | Beste i klassen |
Stripe holder på pengene i 7 dager før utbetaling til din konto. For raskere utbetaling, vurder å søke om "Instant Payouts".
Grunnleggende oppsett
Installer avhengigheter
npm install stripe @stripe/stripe-js
Konfigurer miljøvariabler
Legg til i .env.local:
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_-prefikset eksponerer variabelen til nettleseren.
Bruk ALDRI dette for hemmelige nøkler som STRIPE_SECRET_KEY!
Server-side Stripe-instans
// lib/stripe.ts
import Stripe from "stripe";
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error("STRIPE_SECRET_KEY environment variable is required");
}
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2024-11-20.acacia",
typescript: true,
});
Stripe Checkout
Den enkleste måten å ta imot betalinger er med Stripe Checkout — en ferdig betalingsside som Stripe hoster.
Opprett checkout-sesjon
// app/api/checkout/route.ts
import { auth } from "@/lib/auth";
import { stripe } from "@/lib/stripe";
export async function POST(request: Request) {
const session = await auth.api.getSession({ headers: request.headers });
if (!session) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const { priceId } = await request.json();
const checkoutSession = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
line_items: [
{
price: priceId,
quantity: 1,
},
],
customer_email: session.user.email,
metadata: {
userId: session.user.id,
},
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing?canceled=true`,
});
return Response.json({ url: checkoutSession.url });
}
Client-side redirect
// components/checkout-button.tsx
"use client";
import { toast } from "sonner";
export function CheckoutButton({ priceId }: { priceId: string }) {
const handleCheckout = async () => {
try {
const response = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ priceId }),
});
const { url } = await response.json();
window.location.href = url;
} catch (error) {
toast.error("Noe gikk galt. Prøv igjen.");
}
};
return (
<button onClick={handleCheckout}>
Oppgrader til Premium
</button>
);
}
Webhooks — Kritisk viktig
Webhooks er hvordan Stripe forteller deg at noe har skjedd — en betaling er fullført, et abonnement er kansellert, etc.
Opprett webhook-handler
// app/api/webhooks/stripe/route.ts
import { stripe } from "@/lib/stripe";
import { db } from "@/lib/db";
import { subscriptions } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("stripe-signature")!;
let event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error("Webhook signature verification failed");
return Response.json({ error: "Invalid signature" }, { status: 400 });
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object;
const userId = session.metadata?.userId;
if (userId) {
await db.insert(subscriptions).values({
id: crypto.randomUUID(),
userId,
stripeCustomerId: session.customer as string,
stripeSubscriptionId: session.subscription as string,
status: "active",
});
}
break;
}
case "customer.subscription.updated":
case "customer.subscription.deleted": {
const subscription = event.data.object;
await db
.update(subscriptions)
.set({
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
})
.where(eq(subscriptions.stripeSubscriptionId, subscription.id));
break;
}
}
return Response.json({ received: true });
}
Lokal webhook-testing
I development, bruk Stripe CLI:
# Installer Stripe CLI
brew install stripe/stripe-cli/stripe
# Logg inn
stripe login
# Forward webhooks til localhost
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Stripe CLI gir deg en midlertidig webhook-secret som starter med whsec_.
Bruk denne i .env.local under development.
Abonnementshåndtering
Sjekk abonnementsstatus
// lib/subscription.ts
import { db } from "@/lib/db";
import { subscriptions } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
export async function getSubscription(userId: string) {
const subscription = await db.query.subscriptions.findFirst({
where: eq(subscriptions.userId, userId),
});
if (!subscription) return null;
return {
isActive: subscription.status === "active",
isPastDue: subscription.status === "past_due",
currentPeriodEnd: subscription.currentPeriodEnd,
};
}
Kundeportal for selvbetjening
La brukere administrere sitt abonnement selv:
// app/api/billing/portal/route.ts
import { auth } from "@/lib/auth";
import { stripe } from "@/lib/stripe";
import { db } from "@/lib/db";
import { subscriptions } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
export async function POST(request: Request) {
const session = await auth.api.getSession({ headers: request.headers });
if (!session) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const subscription = await db.query.subscriptions.findFirst({
where: eq(subscriptions.userId, session.user.id),
});
if (!subscription?.stripeCustomerId) {
return Response.json({ error: "No subscription found" }, { status: 404 });
}
const portalSession = await stripe.billingPortal.sessions.create({
customer: subscription.stripeCustomerId,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
});
return Response.json({ url: portalSession.url });
}
Norske betalingsmetoder
Stripe støtter flere betalingsmetoder populære i Norge:
const checkoutSession = await stripe.checkout.sessions.create({
mode: "payment",
payment_method_types: [
"card", // Visa, Mastercard
"klarna", // Klarna (populært i Norge)
"mobilepay", // MobilePay (Danmark/Norge)
],
// ... resten av konfigurasjonen
});
Vipps støttes ikke direkte av Stripe. For Vipps-integrasjon, bruk Vipps' egen løsning eller en gateway som støtter begge.
PCI-compliance
Stripe håndterer all PCI-compliance for deg når du bruker deres løsninger:
- Stripe Checkout — Fullstendig håndtert av Stripe
- Stripe Elements — Kort-input rendres i sikre iframes
- Payment Intents — Kortdata berører aldri din server
Du trenger aldri å se eller lagre kortdetaljer.
Integrasjon med resten av stacken
Stripe-integrasjonen fungerer sømløst med LLM-First Boilerplate-stacken:
- Better Auth — Autentisering for å koble betalinger til brukere
- Drizzle ORM — Lagre abonnementer i PostgreSQL
- Sonner toast — Brukernotifikasjoner for betalingsstatus
Som vi diskuterer i AI-først utvikling, handler god arkitektur om konsistente mønstre. Stripe-integrasjonen følger samme mønster som resten av kodebasen.
Feilhåndtering
Robust feilhåndtering er kritisk for betalinger:
import { toast } from "sonner";
async function handlePayment() {
try {
const response = await fetch("/api/checkout", {
method: "POST",
body: JSON.stringify({ priceId }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Betalingen feilet");
}
const { url } = await response.json();
window.location.href = url;
} catch (error) {
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error("Noe gikk galt. Prøv igjen senere.");
}
}
}
Testing
Stripe tilbyr test-kort for ulike scenarioer:
| Kort | Resultat |
|---|---|
| 4242 4242 4242 4242 | Vellykket betaling |
| 4000 0000 0000 0002 | Avvist kort |
| 4000 0000 0000 9995 | Insufficient funds |
| 4000 0000 0000 3220 | 3D Secure påkrevd |
Bruk hvilken som helst fremtidig utløpsdato og CVC.
Konklusjon
Stripe-integrasjon i Next.js trenger ikke være komplisert. Med riktig oppsett får du:
- Sikker betalingshåndtering uten PCI-bekymringer
- Abonnementer med automatisk fornyelse
- Webhooks for real-time oppdateringer
- Kundeportal for selvbetjening
LLM-First Boilerplate gir deg Stripe ferdig konfigurert, slik at du kan fokusere på å bygge produktet ditt.
God jul og godt salg! 🎄
Les mer
- Kom i gang med LLM-First Boilerplate — Komplett oppsett inkludert Stripe
- Drizzle ORM guide — Database-oppsett for abonnementer
- Deploy til Vercel — Produksjonsklart med Stripe webhooks
Kilder
- Stripe dokumentasjon — Offisiell dokumentasjon
- Stripe CLI — Verktøy for lokal testing
- Next.js Stripe guide — Vercels offisielle guide
- Stripe priser Norge — Oppdaterte transaksjonsgebyrer
Klar til å tjene penger på appen din? Start med LLM-First Boilerplate og ha Stripe-betalinger oppe på timer, ikke dager.