Introduktion: Hvorfor Databaseintegration Betyder Alt i 2026
Hvis du bygger noget seriøst med Next.js i dag, kommer du ikke udenom databaser. Full-stack applikationer er simpelthen blevet normen, og med App Routerens Server Components og Server Actions kan du nu bygge databasedrevne apps uden at rode med separate backend-APIs. Det har ærligt talt ændret hele spillepladen.
Databaseintegration er ikke bare et "nice to have" — det er kernen i næsten enhver moderne webapplikation. Uanset om du bygger en simpel kontaktformular eller en komplet e-handelsplatform, skal dine data gemmes et sted. Og valget af den rette database og ORM? Det kan gøre en kæmpe forskel for både din udviklingshastighed og applikationens performance.
Her kommer Drizzle ORM ind i billedet.
Drizzle har hurtigt etableret sig som det foretrukne valg for TypeScript-udviklere, og det er der gode grunde til. I modsætning til Prisma, der kompilerer queries ved build-time, er Drizzle en letvægts, runtime-first ORM der genererer effektiv SQL uden unødig overhead. Du får fuld TypeScript type-safety, understøttelse af alle moderne databaser, og en API der føles helt naturlig — især hvis du allerede kender SQL.
Det der virkelig gør Drizzle til et oplagt valg, er dets performance i serverless miljøer som Vercel. Nul runtime dependencies og minimal bundle size — det er svært at argumentere imod. Kombiner det med Neon PostgreSQL (som tilbyder serverless Postgres med autoscaling og database branching), og du har en stack der bare fungerer.
Opsætning af Projektet
Okay, lad os komme i gang. Først opretter vi et nyt Next.js projekt med App Router:
npx create-next-app@latest my-drizzle-app
cd my-drizzle-app
Under opsætningen skal du vælge TypeScript, ESLint, Tailwind CSS (valgfrit), og vigtigst af alt: App Router. Derefter installerer vi Drizzle ORM og de nødvendige pakker:
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit dotenv
Drizzle-kit er CLI-værktøjet til migrationer, og @neondatabase/serverless er Neons officielle driver, optimeret til serverless miljøer med WebSocket-baserede forbindelser.
Nu skal vi have en database. Gå til Neons dashboard, opret et nyt projekt, og kopier din connection string. Opret en .env.local fil i projektets rod:
DATABASE_URL=postgresql://username:[email protected]/neondb?sslmode=require
Næste skridt er Drizzle-konfigurationen. Opret drizzle.config.ts i projektets rod:
import type { Config } from 'drizzle-kit';
import * as dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
export default {
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
} satisfies Config;
Denne fil fortæller Drizzle, hvor vores skema bor, hvor migrationsfiler skal lande, og hvordan den forbinder til databasen. Ret ligetil.
Opret mappestrukturen til database-koden:
mkdir -p src/db
touch src/db/schema.ts
touch src/db/index.ts
Drizzle ORM Skemaer og Migrationer
Drizzle bruger en code-first tilgang til skema-definition. I stedet for at skrive rå SQL eller bruge et separat definition-sprog, definerer du tabellerne direkte i TypeScript. Det giver fantastisk type-safety og autocomplete i hele applikationen — og det er ærligt talt en fornøjelse at arbejde med.
Lad os bygge et eksempel-schema til en blog i src/db/schema.ts:
import { pgTable, serial, text, varchar, timestamp, integer, boolean } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
content: text('content').notNull(),
published: boolean('published').default(false).notNull(),
authorId: integer('author_id').notNull().references(() => users.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const comments = pgTable('comments', {
id: serial('id').primaryKey(),
content: text('content').notNull(),
postId: integer('post_id').notNull().references(() => posts.id),
userId: integer('user_id').notNull().references(() => users.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
comments: many(comments),
}));
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
comments: many(comments),
}));
export const commentsRelations = relations(comments, ({ one }) => ({
post: one(posts, {
fields: [comments.postId],
references: [posts.id],
}),
user: one(users, {
fields: [comments.userId],
references: [users.id],
}),
}));
Vi definerer tre tabeller med relationer mellem dem. Bemærk brugen af pgTable til tabeldefinitioner og de forskellige datatyper: serial, varchar, text, timestamp, integer og boolean. Foreign keys sættes med references(), og relations() håndterer objektrelationsmappings til queries.
Drizzle understøtter i øvrigt alle PostgreSQL datatyper — jsonb, array, enum, you name it. For eksempel kan vi tilføje en tags-kolonne:
import { pgTable, text, varchar } from 'drizzle-orm/pg-core';
export const posts = pgTable('posts', {
// ... andre kolonner
tags: text('tags').array(),
metadata: text('metadata').$type<{ views: number; likes: number }>(),
});
Med skemaet på plads kan vi generere migrationer. Tilføj disse scripts til package.json:
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
}
}
Generer migrationerne:
npm run db:generate
Det opretter SQL-migrationsfilerne i drizzle-mappen. For at køre dem mod databasen:
npm run db:push
Du kan også bruge db:migrate for en mere kontrolleret process i produktion. Og så er der Drizzle Studio — et visuelt database-værktøj du starter med npm run db:studio. Det er rigtig praktisk til at inspicere data undervejs.
Forbindelses-pooling i Serverless Miljøer
Her kommer vi til noget, der bider mange udviklere: connection management i serverless miljøer. Hver serverless function er stateless og kortvarig, og hvis du ikke passer på, ender du med connection exhaustion. Det er ikke sjovt.
Traditionelle connection pools fungerer dårligt i serverless, fordi hver function-instans opretter sin egen pool. I stedet skal vi bruge connection pooling på database-siden kombineret med smarte caching-mønstre.
Neon håndterer dette elegant med deres serverless driver. Lad os opsætte vores database-klient i src/db/index.ts:
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
Det her opretter en HTTP-baseret klient, der er perfekt til korte queries. Til længerevarende queries eller transaktioner bruger vi WebSocket-driveren i stedet:
import { drizzle } from 'drizzle-orm/neon-serverless';
import { Pool } from '@neondatabase/serverless';
import * as schema from './schema';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });
I produktion på Vercel er det vigtigt at bruge et globalt singleton-mønster, så vi genbruger connections mellem requests i samme function-instans:
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';
declare global {
var db: ReturnType<typeof drizzle> | undefined;
}
if (!global.db) {
const sql = neon(process.env.DATABASE_URL!);
global.db = drizzle(sql, { schema });
}
export const db = global.db;
Det reducerer latency og connection overhead markant. En lille ting, der gør en stor forskel.
Husk også, at Server Components kører på serveren og kan query databasen direkte, mens Client Components kører i browseren og skal gå igennem Server Actions eller API Routes. Det er en vigtig skelnen, som er let at glemme.
Datahentning med Server Components
Og nu til det virkelig fede: React Server Components lader os hente data direkte i vores komponenter uden separate API-endpoints. Det simplificerer arkitekturen enormt og fjerner en masse boilerplate.
Lad os oprette en Server Component der viser blog-posts. Opret app/posts/page.tsx:
import { db } from '@/db';
import { posts, users } from '@/db/schema';
import { eq } from 'drizzle-orm';
import Link from 'next/link';
export default async function PostsPage() {
const allPosts = await db
.select({
id: posts.id,
title: posts.title,
createdAt: posts.createdAt,
authorName: users.name,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(posts.createdAt);
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Blog Posts</h1>
<div className="space-y-4">
{allPosts.map((post) => (
<article key={post.id} className="border p-4 rounded-lg">
<Link href={`/posts/${post.id}`}>
<h2 className="text-xl font-semibold hover:text-blue-600">
{post.title}
</h2>
</Link>
<p className="text-gray-600">
Af {post.authorName} •
{new Date(post.createdAt).toLocaleDateString('da-DK')}
</p>
</article>
))}
</div>
</div>
);
}
Flere ting er værd at bemærke her. Komponenten er en async function — det er muligt i Server Components. Vi kan await database-queries direkte, og resultatet renderes på serveren, før det sendes til klienten. Ingen loading states, ingen useEffect, ingen client-side fetching.
Drizzles query-builder giver fuld type-safety. TypeScript ved præcis hvilke kolonner vi henter når vi kalder select(), og autocomplete fungerer upåklageligt.
For at hente en enkelt post med relationer kan vi bruge Drizzles relational query API. Opret app/posts/[id]/page.tsx:
import { db } from '@/db';
import { posts } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { notFound } from 'next/navigation';
export default async function PostPage({
params,
}: {
params: { id: string };
}) {
const postId = parseInt(params.id);
const post = await db.query.posts.findFirst({
where: eq(posts.id, postId),
with: {
author: true,
comments: {
with: {
user: true,
},
orderBy: (comments, { desc }) => [desc(comments.createdAt)],
},
},
});
if (!post) {
notFound();
}
return (
<article className="container mx-auto px-4 py-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<p className="text-gray-600 mb-8">
Af {post.author.name} •
{new Date(post.createdAt).toLocaleDateString('da-DK')}
</p>
<div className="prose max-w-none mb-12">{post.content}</div>
<section>
<h2 className="text-2xl font-bold mb-4">
Kommentarer ({post.comments.length})
</h2>
<div className="space-y-4">
{post.comments.map((comment) => (
<div key={comment.id} className="border-l-4 border-gray-300 pl-4">
<p className="font-semibold">{comment.user.name}</p>
<p className="text-gray-700">{comment.content}</p>
<p className="text-sm text-gray-500">
{new Date(comment.createdAt).toLocaleDateString('da-DK')}
</p>
</div>
))}
</div>
</section>
</article>
);
}
Den relational query API gør nested relationer utrolig nemme. with-parameteren specificerer hvilke relationer der skal med, og vi kan sortere comments direkte i queryen. Alt sammen type-safe og effektivt.
Next.js cacher automatisk data fra Server Components. Identiske queries inden for samme request udføres kun én gang. Her er de vigtigste caching-strategier:
// Static generation (default) - cached indtil næste build
export default async function StaticPage() {
const data = await db.query.posts.findMany();
return <div>...</div>;
}
// Revalidate hver 60 sekunder
export const revalidate = 60;
// Dynamic rendering - ingen caching
export const dynamic = 'force-dynamic';
// Opt-out af caching for specifikke queries
import { unstable_noStore as noStore } from 'next/cache';
export default async function DynamicPage() {
noStore();
const data = await db.query.posts.findMany();
return <div>...</div>;
}
Datamutation med Server Actions
Server Actions er Next.js' svar på form submissions og datamutationer — helt uden API-routes. De er asynkrone funktioner der kører på serveren og kan kaldes direkte fra Client Components eller formularer. Og ja, det er lige så elegant som det lyder.
Lad os oprette en Server Action til nye blog-posts. Først installerer vi Zod til validering:
npm install zod
Opret app/actions/posts.ts:
'use server';
import { db } from '@/db';
import { posts } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
const createPostSchema = z.object({
title: z.string().min(1, 'Titel er påkrævet').max(255, 'Titel er for lang'),
content: z.string().min(10, 'Indhold skal være mindst 10 tegn'),
authorId: z.number().int().positive(),
});
export async function createPost(prevState: any, formData: FormData) {
const validatedFields = createPostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
authorId: parseInt(formData.get('authorId') as string),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}
const { title, content, authorId } = validatedFields.data;
try {
const [newPost] = await db
.insert(posts)
.values({
title,
content,
authorId,
published: false,
})
.returning();
revalidatePath('/posts');
redirect(`/posts/${newPost.id}`);
} catch (error) {
return {
errors: {
_form: ['Der opstod en fejl ved oprettelse af post'],
},
};
}
}
export async function updatePost(postId: number, formData: FormData) {
const validatedFields = createPostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
authorId: parseInt(formData.get('authorId') as string),
});
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
};
}
try {
await db
.update(posts)
.set({
...validatedFields.data,
updatedAt: new Date(),
})
.where(eq(posts.id, postId));
revalidatePath(`/posts/${postId}`);
revalidatePath('/posts');
return { success: true };
} catch (error) {
return {
errors: {
_form: ['Der opstod en fejl ved opdatering af post'],
},
};
}
}
export async function deletePost(postId: number) {
try {
await db.delete(posts).where(eq(posts.id, postId));
revalidatePath('/posts');
redirect('/posts');
} catch (error) {
return {
errors: {
_form: ['Der opstod en fejl ved sletning af post'],
},
};
}
}
export async function publishPost(postId: number) {
try {
await db
.update(posts)
.set({ published: true })
.where(eq(posts.id, postId));
revalidatePath(`/posts/${postId}`);
revalidatePath('/posts');
return { success: true };
} catch (error) {
return {
errors: {
_form: ['Der opstod en fejl ved publicering af post'],
},
};
}
}
Der er flere vigtige mønstre her. Filen starter med 'use server' directive, som markerer funktionerne som Server Actions. Vi bruger Zod til validering, hvilket giver type-safe input-check og gode fejlmeddelelser.
revalidatePath() er afgørende — uden den ville brugere se forældet data efter opdateringer. Det er en fejl jeg selv har lavet mere end én gang, og det kan være svært at debugge.
Nu kan vi lave en form-komponent. Opret app/posts/new/page.tsx:
'use client';
import { createPost } from '@/app/actions/posts';
import { useFormState, useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{pending ? 'Opretter...' : 'Opret Post'}
</button>
);
}
export default function NewPostPage() {
const [state, formAction] = useFormState(createPost, { errors: {} });
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-6">Opret Ny Post</h1>
<form action={formAction} className="max-w-2xl space-y-4">
<input type="hidden" name="authorId" value="1" />
<div>
<label htmlFor="title" className="block font-medium mb-2">
Titel
</label>
<input
type="text"
id="title"
name="title"
className="w-full px-3 py-2 border rounded"
/>
{state.errors?.title && (
<p className="text-red-600 text-sm mt-1">
{state.errors.title[0]}
</p>
)}
</div>
<div>
<label htmlFor="content" className="block font-medium mb-2">
Indhold
</label>
<textarea
id="content"
name="content"
rows={10}
className="w-full px-3 py-2 border rounded"
/>
{state.errors?.content && (
<p className="text-red-600 text-sm mt-1">
{state.errors.content[0]}
</p>
)}
</div>
{state.errors?._form && (
<p className="text-red-600">{state.errors._form[0]}</p>
)}
<SubmitButton />
</form>
</div>
);
}
Komponenten bruger useFormState til form state og fejlhåndtering, plus useFormStatus til at vise en loading-tilstand under submission. Simpelt og effektivt.
Optimistiske Opdateringer og Fejlhåndtering
Vil du have en virkelig responsiv brugeroplevelse? Så skal du implementere optimistiske opdateringer, hvor UI'et opdateres øjeblikkeligt — før serveren overhovedet har svaret. React 19's useOptimistic hook gør det overraskende nemt.
Her er en like-knap med optimistisk opdatering:
'use client';
import { useOptimistic, useTransition } from 'react';
import { likePost } from '@/app/actions/posts';
export function LikeButton({
postId,
initialLikes,
}: {
postId: number;
initialLikes: number;
}) {
const [isPending, startTransition] = useTransition();
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state, amount: number) => state + amount
);
async function handleLike() {
startTransition(async () => {
addOptimisticLike(1);
await likePost(postId);
});
}
return (
<button
onClick={handleLike}
disabled={isPending}
className="flex items-center space-x-2 px-4 py-2 border rounded
hover:bg-gray-50 disabled:opacity-50"
>
<span>❤</span>
<span>{optimisticLikes} likes</span>
</button>
);
}
useOptimistic opdaterer state lokalt med det samme, mens useTransition holder UI'et responsivt under den asynkrone operation. Og det bedste? Hvis serveren fejler, ruller React automatisk tilbage. Pænt.
For fejlhåndtering generelt bør vi bruge error boundaries. Opret app/posts/error.tsx:
'use client';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error('Database error:', error);
}, [error]);
return (
<div className="container mx-auto px-4 py-8">
<div className="max-w-md mx-auto text-center">
<h2 className="text-2xl font-bold text-red-600 mb-4">
Noget gik galt!
</h2>
<p className="text-gray-600 mb-6">
Der opstod en fejl ved indlæsning af data. Prøv venligst igen.
</p>
<button
onClick={reset}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Prøv igen
</button>
</div>
</div>
);
}
Placer denne fil ved siden af dine page.tsx filer, og Next.js fanger automatisk fejl fra Server Components. Det er en af de ting der "bare virker".
Til database-specifikke fejl kan det være en god idé at implementere retry-logik med exponential backoff:
async function queryWithRetry<T>(
queryFn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
let lastError: Error;
for (let i = 0; i < maxRetries; i++) {
try {
return await queryFn();
} catch (error) {
lastError = error as Error;
if (i < maxRetries - 1) {
const delay = baseDelay * Math.pow(2, i);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw lastError!;
}
// Eksempel på brug:
const posts = await queryWithRetry(() =>
db.query.posts.findMany()
);
Avancerede Mønstre: Transaktioner og Relationer
I mere komplekse applikationer har vi tit brug for at udføre flere database-operationer atomisk. Transaktioner sikrer at enten lykkes alt, eller også sker intet. Det er kritisk for data-integritet.
Her er en Server Action der opretter en bruger og deres første post i én transaktion:
'use server';
import { db } from '@/db';
import { users, posts } from '@/db/schema';
import { revalidatePath } from 'next/cache';
export async function createUserWithPost(
userData: { name: string; email: string },
postData: { title: string; content: string }
) {
try {
const result = await db.transaction(async (tx) => {
// Opret bruger
const [newUser] = await tx
.insert(users)
.values(userData)
.returning();
// Opret post for den nye bruger
const [newPost] = await tx
.insert(posts)
.values({
...postData,
authorId: newUser.id,
published: true,
})
.returning();
return { user: newUser, post: newPost };
});
revalidatePath('/posts');
return { success: true, data: result };
} catch (error) {
console.error('Transaction failed:', error);
return {
success: false,
error: 'Kunne ikke oprette bruger og post',
};
}
}
Hvis post-oprettelsen fejler, rulles brugeren automatisk tilbage. Ingen orphaned data. Det er præcis den slags sikkerhedsnet man har brug for i produktion.
Drizzles relational query API gør komplekse relationer overraskende tilgængelige. Her er et eksempel med nested relationer og filtrering:
import { db } from '@/db';
import { posts, comments } from '@/db/schema';
import { and, gte, desc, eq } from 'drizzle-orm';
export async function getActiveUsersWithRecentActivity() {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
return await db.query.users.findMany({
with: {
posts: {
where: and(
eq(posts.published, true),
gte(posts.createdAt, thirtyDaysAgo)
),
with: {
comments: {
limit: 5,
orderBy: desc(comments.createdAt),
with: {
user: true,
},
},
},
orderBy: desc(posts.createdAt),
},
},
});
}
Det henter brugere med deres publicerede posts fra de seneste 30 dage, inklusiv de nyeste 5 kommentarer per post — med kommentarforfatterne. Alt i én effektiv query. Type-safe hele vejen igennem.
Til endnu mere komplekse queries kan vi kombinere relational API med rå SQL via Drizzles template tag:
import { sql } from 'drizzle-orm';
import { db } from '@/db';
import { posts, users, comments } from '@/db/schema';
import { eq } from 'drizzle-orm';
export async function getPostStatistics() {
return await db
.select({
authorId: posts.authorId,
authorName: users.name,
totalPosts: sql<number>`count(distinct ${posts.id})`,
totalComments: sql<number>`count(distinct ${comments.id})`,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.leftJoin(comments, eq(posts.id, comments.postId))
.groupBy(posts.authorId, users.name);
}
Drizzles sql template tag lader os skrive rå SQL når det er nødvendigt — men stadig med type-safety og SQL-injection beskyttelse. Det er en god balance mellem fleksibilitet og sikkerhed.
Deployment til Vercel med Neon PostgreSQL
Så er vi nået til deployment. Når applikationen er klar, skal vi have den ud på Vercel.
I Vercel dashboard, gå til dit projekt og tilføj DATABASE_URL som environment variable. Brug Neons pooled connection string til produktion:
DATABASE_URL=postgresql://username:[email protected]/neondb?sslmode=require
Neon tilbyder forskellige connection strings til forskellige formål. Til Vercel anbefales den pooled variant, som automatisk håndterer connection pooling via den integrerede PgBouncer.
For migrationer er der to tilgange:
1. Kør migrationer manuelt før deployment:
npm run db:push
2. Automatiser migrationer i build-processen. Tilføj til package.json:
{
"scripts": {
"build": "npm run db:migrate && next build",
"db:migrate": "tsx ./scripts/migrate.ts"
}
}
Og opret scripts/migrate.ts:
import { drizzle } from 'drizzle-orm/neon-http';
import { migrate } from 'drizzle-orm/neon-http/migrator';
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
const db = drizzle(sql);
async function main() {
console.log('Kører migrationer...');
await migrate(db, { migrationsFolder: './drizzle' });
console.log('Migrationer gennemført!');
process.exit(0);
}
main().catch((err) => {
console.error('Migration fejlede!', err);
process.exit(1);
});
Et pro-tip: Neons branching-feature giver hver Git-branch sin egen database-kopi. Det er genialt til at teste migrationer i isolation, før de rammer main.
Her er en komplet production checklist:
- Verificer at alle environment variabler er sat korrekt i Vercel
- Test migrationer i et staging-miljø først
- Implementer en database backup-strategi
- Konfigurer monitoring og alerting for database-fejl
- Sæt passende revalidation-strategier for cached data
- Implementer rate limiting for Server Actions
- Brug Vercels Edge Config for feature flags
- Overvej Vercels Postgres som et enklere alternativ
For monitoring kan vi slå logging til i development:
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, {
logger: process.env.NODE_ENV === 'development',
});
I produktion bør du integrere med en observability-platform som Sentry eller Datadog til at tracke performance og fejl i realtid. Det er en investering der betaler sig selv hjem hurtigt.
Konklusion og Bedste Praksis
Vi har dækket en masse grund her — fra opsætning og skemaer til Server Components, Server Actions, transaktioner og deployment. Drizzle ORM med Next.js App Router er efter min mening en af de bedste full-stack kombinationer, du kan vælge i 2026.
Lad os opsummere de vigtigste takeaways:
Type-Safety og Validering
Brug TypeScript og Drizzles type-safe API til at fange fejl ved compile-time. Kombiner det med Zod-validering i Server Actions for runtime-beskyttelse. Det dobbelte lag fanger både udvikler-fejl og ondsindet input.
Performance-Optimering
Connection management er kritisk i serverless. Brug Neons driver, implementer singleton-mønstre, og vær strategisk med caching. Husk revalidatePath() efter mutationer — ellers sidder dine brugere og kigger på forældet data.
Fejlhåndtering
Implementer fejlhåndtering på alle niveauer: error boundaries i React, strukturerede fejl fra Server Actions, og retry-logik for forbigående database-fejl. Log alt til en centraliseret platform.
Sikkerhed
Drizzle beskytter mod SQL-injection, men du skal stadig validere input. Og vigtigst af alt — tjek altid brugerrettigheder før database-operationer:
'use server';
import { auth } from '@/lib/auth';
import { db } from '@/db';
import { posts } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
export async function deletePostSecure(postId: number) {
const session = await auth();
if (!session?.user) {
return { error: 'Ikke autoriseret' };
}
const post = await db.query.posts.findFirst({
where: eq(posts.id, postId),
});
if (post?.authorId !== session.user.id) {
return { error: 'Ikke autoriseret til at slette denne post' };
}
await db.delete(posts).where(eq(posts.id, postId));
revalidatePath('/posts');
return { success: true };
}
Skalerbarhed
Design dine skemaer med skalerbarhed for øje. Tilføj indexes på kolonner du query ofte, implementer pagination for store datasæt, og overvej denormalisering for read-heavy scenarier. Drizzle gør indexes nemme:
import { pgTable, serial, varchar, integer, index } from 'drizzle-orm/pg-core';
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
authorId: integer('author_id').notNull(),
}, (table) => ({
authorIdx: index('author_idx').on(table.authorId),
titleIdx: index('title_idx').on(table.title),
}));
Testing
Test dine database-operationer grundigt med en separat test-database. Her er et simpelt seed-eksempel:
import { db } from '@/db';
import { users, posts } from '@/db/schema';
export async function seedTestData() {
const [testUser] = await db
.insert(users)
.values({
name: 'Test Bruger',
email: '[email protected]',
})
.returning();
await db.insert(posts).values([
{
title: 'Test Post 1',
content: 'Indhold...',
authorId: testUser.id,
},
{
title: 'Test Post 2',
content: 'Indhold...',
authorId: testUser.id,
},
]);
}
Fremtidige Overvejelser
Økosystemet udvikler sig konstant. Hold øje med Partial Prerendering, forbedringer i React Server Components, og Drizzles roadmap. Følg Vercel og Neons best practices — de opdaterer jævnligt deres anbefalinger baseret på real-world mønstre.
Med disse principper og mønstre i baghånden er du godt rustet til at bygge robuste, performante full-stack applikationer. Drizzle ORM har bevist sit værd som det rette valg for TypeScript-udviklere — det er hurtigt, type-safe, og føles naturligt for alle der kender SQL. Sammen med Next.js App Router og Neon PostgreSQL har du en moderne stack der balancerer udvikleroplevelse, performance og skalerbarhed.