Databas med Next.js
Prisma, Drizzle, Supabase och SQLite med Next.js: när du använder vilket, connection pooling i serverless och en tydlig rekommendation.
Databasintegration i Next.js ställer dig inför två separata val: vilken databas du lagrar data i, och vilket bibliotek din kod pratar med databasen genom. Det första valet är lätt att ändra i efterhand. Det andra sätter tonen för hela kodbasen.
Den här guiden täcker de viktigaste alternativen på båda nivåerna, går igenom connection pooling (det problem som saboterar serverless-appar snabbare än något annat), och avslutar med en tydlig rekommendation för de vanligaste scenarierna.
Databaslandskapet
Börja med att skilja på de två lagren.
Lagret för var datan bor är din faktiska databas. För de flesta webbapplikationer är Postgres standardvalet, och det finns flera managed alternativ: Neon erbjuder serverless Postgres med en generös free tier och en funktion för databas-branching som gör preview-miljöer enkla. Supabase tillhandahåller managed Postgres som en del av en bredare plattform med autentisering, fillagring och realtidsabonnemang. PlanetScale körde MySQL på Vitess och var länge populärt, men är numera betalbaserat utan free tier.
SQLite är ett annat spår: ingen serverprocess, data i en fil. För lokal utveckling och enkla VPS-driftsättningar räcker better-sqlite3 långt. Turso tar SQLite ett steg längre med libSQL-protokollet som distribuerar databasen globalt, med läsningar från närmaste edge-plats.
Självhostad Postgres ger full kontroll, men innebär att du hanterar backups, uppgraderingar och tillgänglighet själv. Det är ett rimligt val för team med infrastrukturkompetens.
Lagret för hur din kod pratar med databasen är ORM:en eller klienten. Det här valet påverkar din vardag mer än vilket Postgres-alternativ du väljer, eftersom de flesta hosted Postgres-tjänster är i stort sett utbytbara. De intressanta besluten är: Prisma mot Drizzle mot rå SQL, och om en managed plattform som Supabase är värd sina opinioner.
Prisma
Prisma bygger på ett schema-first-flöde. Du definierar dina modeller i schema.prisma, kör prisma migrate dev för att skapa och applicera migreringar, och arbetar sedan med den genererade, typesäkra klienten. Det är ett högnivå-abstraktion som handlar mer om modeller och relationer än om SQL-syntax.
// prisma/schema.prisma
model Inlagg {
id String @id @default(cuid())
titel String
innehall String
skapad DateTime @default(now())
}
Prisma-klienten är en singleton. Det är viktigt att förstå varför. I Next.js skapar Hot Module Replacement under utveckling nya modulinstanser vid varje sparad fil. Utan singletönmönstret öppnas en ny databasanslutning vid varje reload, och du tömmer snabbt din anslutningspool. I produktion, där Node.js-processen lever länge, är en enda instans standardbeteendet.
// lib/prisma.js
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis;
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
Med klienten på plats är en Server Component som hämtar data rak:
// app/inlagg/page.jsx
import { prisma } from "@/lib/prisma";
export default async function Inlagg() {
const inlagg = await prisma.inlagg.findMany({
orderBy: { skapad: "desc" },
take: 10,
});
return (
<ul>
{inlagg.map((i) => (
<li key={i.id}>{i.titel}</li>
))}
</ul>
);
}
Prisma genererar TypeScript-typer för hela datamodellen automatiskt. Det innebär att om du lägger till ett fält i schemat, uppdaterar klienten och glömmer att hantera det nya fältet i ett formulär, fångar TypeScript felet vid kompilering.
Nackdelarna är reella: Prisma har ett binary beroende (query engine) som ökar cold start-tider i edge-miljöer, och paketets storlek är märkbart. Prisma Accelerate löser en del av dessa problem, men det är en extra tjänst att hantera.
Passar bra för: team, komplexa relationella scheman, projekt där de genererade TypeScript-typerna för hela datamodellen är värda installationskostnaden.
Drizzle
Drizzle är en tunn ORM med SQL-nära syntax och TypeScript-typer som härleds direkt från schemadefinitionen. Inget kodgenereringssteg, inga binära beroenden. Det fungerar i edge-miljöer som Cloudflare Workers och håller paketstorleken liten.
Du definierar tabeller som JavaScript-objekt:
// lib/schema.js
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
export const inlagg = pgTable("inlagg", {
id: text("id").primaryKey(),
titel: text("titel").notNull(),
skapad: timestamp("skapad").defaultNow(),
});
Drizzle stöder flera databasklienter. Med Neon ser konfigurationen ut så här:
// lib/db.js
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL);
export const db = drizzle(sql);
Querysyntaxen är medvetet SQL-lik. Om du kan SQL kan du läsa Drizzle-kod utan inlärningskurva:
import { db } from "@/lib/db";
import { inlagg } from "@/lib/schema";
import { desc } from "drizzle-orm";
const senaste = await db
.select()
.from(inlagg)
.orderBy(desc(inlagg.skapad))
.limit(10);
Migreringarna hanteras av Drizzle Kit via drizzle-kit generate och drizzle-kit migrate. Det är ett separat CLI-verktyg men följer samma schema som din appkod, så typerna är alltid synkroniserade med databasen.
En sak att notera: Drizzle är yngre än Prisma och ekosystemet är fortfarande i snabb rörelse. Dokumentationen är bra men täcker inte alla kantfall lika grundligt.
Passar bra för: utvecklare som är bekväma med SQL, edge-driftsättningar, prestandakänsliga applikationer där paketstorleken spelar roll, projekt där du vill att ORM:en ska vara tunn och transparent.
Supabase
Supabase är inte bara en databasklient. Det är managed Postgres plus autentisering plus fillagring plus edge functions plus realtidsabonnemang i en enda plattform. JavaScript-klienten pratar med Supabase REST och Realtime API:er snarare än direkt mot Postgres.
// lib/supabase.js - körs enbart på servern, aldrig i klientkod
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY,
);
En enkel server-side-query ser ut så här:
const { data: inlagg } = await supabase
.from("inlagg")
.select("*")
.order("skapad", { ascending: false })
.limit(10);
Det finns två viktiga nycklar att hålla reda på. anon-nyckeln är säker att använda på klientsidan och respekterar Row Level Security. service_role-nyckeln kringgår RLS och ska bara användas server-side, aldrig exponeras i klientkod.
Row Level Security, RLS, är säkerhetspolicyer definierade direkt i databasen. De styr vilka rader en användare kan läsa eller skriva baserat på identitet. Supabase gör det enkelt att konfigurera RLS via dashboarden, och det integrerar sömlöst med Supabase Auth. Exempel: en policy som säger att en användare bara kan läsa sina egna rader tar tre rader SQL att definiera och aktiveras med ett klick.
Tradeoffs med Supabase: du är mer beroende av plattformen. Att migrera bort innebär att återskapa auth, storage och RLS-logik i ett annat system. Fördelen är att du slipper sätta upp och underhålla allt det separat.
Passar bra för: snabb prototyping, projekt som behöver realtime (levande cursors, chatt, dashboards), team som vill ha auth och fillagring i samma plattform.
SQLite och Turso
SQLite är en filbaserad databas utan serverprocess. För små projekt och lokal utveckling är det ofta det enklaste valet. Inga anslutningar att hantera, ingen daemon att starta, datan lever i en .db-fil.
Turso distribuerar SQLite globalt via libSQL-protokollet. Läsningar sker från närmaste edge-plats, vilket gör det till ett rimligt val för edge-driftsättningar. Skrivningar dirigeras fortfarande till en primär nod.
// lib/db.js
import { createClient } from "@libsql/client";
export const db = createClient({
url: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
});
const result = await db.execute(
"SELECT * FROM inlagg ORDER BY skapad DESC LIMIT 10",
);
För lokal utveckling utan Turso-konto kan du använda en lokal fil direkt:
export const db = createClient({
url: "file:./local.db",
});
Turso fungerar med Drizzle och Prisma via deras libSQL-adapters, så du behöver inte skriva rå SQL om du inte vill.
Passar bra för: enkla projekt med okomplicerade scheman, edge-driftsättningar på Cloudflare Workers, applikationer där infrastrukturens enkelhet väger tyngre än Postgres flexibilitet.
Connection pooling i serverless
Det problem som saboterar serverless-appar med traditionella databaser förbises oftare än något annat.
I en traditionell servermiljö, en lång-levande Node.js-process, öppnar du en liten pool med databasanslutningar vid uppstart och återanvänder dem. En pool på tio anslutningar räcker för hundratals requests per sekund.
I serverless är modellen annorlunda. Varje function-invokation kan öppna en ny databasanslutning. Postgres hanterar vanligtvis 100 till 300 samtida anslutningar beroende på plan och inställningar. Under last med många parallella anrop kan du nå det gränsvärdet snabbt. Nya anslutningar börjar misslyckas, requests tidout, och din app ser intermittenta fel utan tydlig orsak.
Lösningarna varierar beroende på stack:
Neon har en inbyggd connection pooler. I Neon-dashboarden hittar du en separat poolad anslutningssträng. Den ser nästan identisk ut men pekar mot Neons pooler-endpoint snarare än direkt mot databasen. Använd den poolade URL:en i din körande applikation.
Prisma Accelerate är Prismas eget pooling och caching-lager. Det fungerar med vilken Postgres-databas som helst: du byter ut databas-URL:en mot en Accelerate-URL och Prisma sköter resten. Det lägger till en extra hop men löser connection pooling och ger HTTP-baserade queries som fungerar i edge-miljöer.
PgBouncer är det klassiska alternativet för självhostad infrastruktur. Det är en connection pooler du kör som en separat process framför din Postgres-instans. Vanligt i traditionell infrastruktur, sällan det du vill hantera i ett managed cloud-scenario.
Supabase kör PgBouncer internt. Använd den poolade anslutningsadressen från Supabase-dashboarden, inte den direkta. Supabase visar båda tydligt under Database-inställningarna.
Med Neon och Prisma ser den rekommenderade konfigurationen ut så här. Två separata variabler i .env:
# .env
# Poolad anslutning - används av applikationen i produktion
DATABASE_URL="postgresql://...@ep-xxx-pooler.eu-central-1.aws.neon.tech/neondb?sslmode=require&pgbouncer=true"
# Direkt anslutning - används av Prisma migrate och introspection
DATABASE_URL_UNPOOLED="postgresql://...@ep-xxx.eu-central-1.aws.neon.tech/neondb?sslmode=require"
I praktiken är DATABASE_URL den poolade adressen och DATABASE_URL_UNPOOLED den direkta. Kör prisma migrate deploy mot den opoolade adressen, applikationen använder den poolade. Anledningen: Prisma-migreringar kräver en direkt anslutning med transaktionsstöd som inte alltid fungerar korrekt via en connection pooler i transaction mode.
I schema.prisma lägger du till:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DATABASE_URL_UNPOOLED")
}
directUrl talar om för Prisma att använda den opoolade adressen specifikt för migreringar och introspection, medan applikationskoden använder url.
Rekommendation
För de flesta nya projekt är Prisma och Neon det starkaste kombinationsvalet. Schema-first-flödet, genererade typer och migreringsverktyget minskar risken för fel på komplexa datamodeller. Neons free tier och inbyggda connection pooling eliminerar det klassiska serverless-anslutningsproblemet utan extra tjänster.
Välj Drizzle när edge-driftsättning eller paketstorleken är ett hårt krav, eller när du föredrar att uttrycka queries i SQL-nära syntax utan abstraktioner. Drizzle fungerar utmärkt med Neon, Turso och andra drivers.
Välj Supabase när projektet behöver realtid, när du vill ha autentisering och fillagring inkluderat från start, eller när du bygger en snabb prototyp och vill minimera antalet separata tjänster att konfigurera.
Välj SQLite och Turso för enkla, edge-first projekt eller när infrastrukturkomplexiteten hos Postgres inte är motiverad av projektets storlek. Turso passar särskilt bra med Cloudflare Workers och Drizzle.
Oavsett vilket alternativ du väljer: konfigurera connection pooling från dag ett om du kör serverless. Det är enkelt att glömma under utveckling och kostsamt att felsöka under produktion.
