/ astro-integrations / How to Integrate Drizzle ORM with Astro: Complete Guide
astro-integrations 10 min read

How to Integrate Drizzle ORM with Astro: Complete Guide

Step-by-step guide to integrating Drizzle ORM with your Astro website. Setup, configuration, and best practices.

How to Integrate Drizzle ORM with Astro: Complete Guide

How to Integrate Drizzle ORM with Astro: Complete Guide

Drizzle ORM is a lightweight, type-safe SQL toolkit for TypeScript that generates zero runtime overhead. Unlike heavier ORMs that abstract away SQL entirely, Drizzle lets you write queries that map directly to SQL while giving you full TypeScript autocompletion and type checking. Combined with Astro's server-side capabilities, it provides a solid data layer for dynamic websites and applications.

This guide covers setting up Drizzle ORM in an Astro project from scratch, defining schemas, running queries, and handling migrations. It is written against Drizzle ORM 0.45.2, Drizzle Kit 0.31.10, the postgres driver 3.4.9, and Astro 6.4.2 (versions current as of 2026-05-29).

Prerequisites

Before getting started, you will need:

  • Node.js 22.12.0 or higher (Astro 6 declares engines.node as >=22.12.0, so Node 22 LTS is the floor)
  • An existing Astro project (Astro 6.x assumed throughout this guide)
  • A supported database: PostgreSQL, MySQL, or SQLite
  • Basic TypeScript knowledge
  • An adapter installed so individual pages and endpoints can render on demand (Node, Vercel, Netlify, or Cloudflare)

This guide uses PostgreSQL as the database, but Drizzle supports all three databases with the same API patterns.

A Note on Astro Rendering Modes

In current Astro, your whole site is prerendered to static HTML by default (output: 'static'). The old output: 'hybrid' value was removed in Astro 5, so do not add it to astro.config.mjs. Static is now the baseline, and you opt individual routes into server rendering instead of opting them out.

Because querying a database has to happen on the server at request time, any page or endpoint that talks to Drizzle must opt out of prerendering with export const prerender = false, and your project needs an on-demand rendering adapter. If most of your site is database-driven, set output: 'server' in astro.config.mjs and mark the pages you still want static with export const prerender = true.

Installation

Install Drizzle ORM, the Drizzle Kit CLI for migrations, and the PostgreSQL driver:

npm install drizzle-orm postgres
npm install -D drizzle-kit

If you are using MySQL or SQLite instead, swap postgres for mysql2 (currently 3.22.4) or better-sqlite3 (currently 12.10.0) respectively.

Make sure your Astro project has an on-demand rendering adapter installed. The astro add command installs the adapter and wires it into your config automatically:

npx astro add node

This pulls in @astrojs/node (currently 10.1.2) and adds it to astro.config.mjs. The official adapters are @astrojs/node, @astrojs/vercel, @astrojs/netlify, and @astrojs/cloudflare. You only need an adapter for the routes that render on demand. Pages that do not touch the database can stay static.

Configuration

Environment Variables

Add your database connection string to .env:

DATABASE_URL=postgresql://username:password@localhost:5432/your_database

Drizzle Configuration File

Create drizzle.config.ts in your project root. This tells Drizzle Kit where your schema files are and how to connect to the database for migrations:

// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

Defining Your Schema

Create a schema file that describes your database tables using Drizzle's TypeScript-first approach:

// src/db/schema.ts
import { pgTable, serial, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core';

export const categories = pgTable('categories', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  slug: text('slug').notNull().unique(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  slug: text('slug').notNull().unique(),
  content: text('content'),
  published: boolean('published').default(false),
  categoryId: integer('category_id').references(() => categories.id),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow(),
});

The categoryId column carries a foreign key to categories.id. It is what the relational query example later in this guide reads from, so define it now. categories is declared before posts so the references() callback can resolve it.

Creating the Database Client

Set up a reusable database client that your Astro pages and API routes can import:

// src/db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';

const client = postgres(import.meta.env.DATABASE_URL);
export const db = drizzle(client, { schema });

Running Migrations

Generate migration files from your schema and apply them:

npx drizzle-kit generate
npx drizzle-kit migrate

This creates SQL migration files in the ./drizzle folder and runs them against your database. You can also use npx drizzle-kit push during development to push schema changes directly without generating migration files.

Common Patterns

Querying Data in Astro Pages

Import the database client and use Drizzle's query builder in your page frontmatter:

---
// src/pages/blog.astro
export const prerender = false; // render on demand so the query runs per request

import { db } from '../db';
import { posts } from '../db/schema';
import { eq, desc } from 'drizzle-orm';

const allPosts = await db
  .select()
  .from(posts)
  .where(eq(posts.published, true))
  .orderBy(desc(posts.createdAt));
---

<html>
  <body>
    <h1>Blog</h1>
    {allPosts.map((post) => (
      <article>
        <h2><a href={`/blog/${post.slug}`}>{post.title}</a></h2>
        <time>{post.createdAt?.toLocaleDateString()}</time>
      </article>
    ))}
  </body>
</html>

Every query is fully typed. Your editor will autocomplete column names and catch type errors. The export const prerender = false line is what tells Astro to run this page on the server at request time instead of trying to execute the database call during the static build.

API Routes with Mutations

Create API endpoints that insert or update data:

// src/pages/api/posts.ts
import type { APIRoute } from 'astro';
import { db } from '../../db';
import { posts } from '../../db/schema';

export const prerender = false; // endpoints that mutate data render on demand

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json();

  const newPost = await db.insert(posts).values({
    title: body.title,
    slug: body.slug,
    content: body.content,
  }).returning();

  return new Response(JSON.stringify(newPost[0]), {
    status: 201,
    headers: { 'Content-Type': 'application/json' },
  });
};

Relational Queries

Drizzle supports relational queries when you define relations in your schema. This lets you fetch related data in a single query without manual joins. Relations are declared with the relations() helper, and the official docs are explicit that you must pass every table and relation into drizzle() on initialization for the db.query API to work. The client created earlier already does that with drizzle(client, { schema }), so the db.query API is ready once you add the relations below:

// src/db/schema.ts (add relations)
import { relations } from 'drizzle-orm';

export const postsRelations = relations(posts, ({ one }) => ({
  category: one(categories, {
    fields: [posts.categoryId],
    references: [categories.id],
  }),
}));

export const categoriesRelations = relations(categories, ({ many }) => ({
  posts: many(posts),
}));

With relations in place, read related rows through the db.query API instead of writing a join by hand:

const postsWithCategory = await db.query.posts.findMany({
  with: {
    category: true,
  },
});

Common Errors and Fixes

"Cannot find module 'postgres'" at runtime: Make sure you installed the postgres package (not pg). The drizzle-orm/postgres-js adapter imported in the client requires the postgres npm package specifically. The Drizzle get-started guide pairs drizzle-orm/postgres-js with import postgres from 'postgres'.

The page renders during build and the query throws, or the build hangs on the database call: A page that queries Drizzle was left as the default static route. Static pages execute their frontmatter at build time, so the database call fires when no server is running. Add export const prerender = false to that page or endpoint, and confirm an adapter is installed. Astro prerenders the entire site by default, so anything that needs a request-time server has to opt out explicitly.

"Cannot use output: 'hybrid'" or the config is rejected: output: 'hybrid' was removed in Astro 5 and is not valid in Astro 6. The only valid output values are 'static' (the default) and 'server'. Delete the hybrid line. Keep static as the default and use export const prerender = false per route, or switch to output: 'server' and mark static routes with export const prerender = true.

No adapter installed but a route is on demand: On-demand rendering needs an adapter for your deployment target. Run npx astro add node (or vercel, netlify, cloudflare) so Astro can build the server entry point.

db.query is undefined or relations are missing: The relational query API only works when every table and relation from your schema is passed into drizzle() on initialization. Confirm your client uses drizzle(client, { schema }) with import * as schema from './schema', and that you exported the relations() definitions from the schema file.

Type errors on schema columns: Drizzle infers types from your schema definition. If you added a column but see type errors, regenerate your migration files with drizzle-kit generate and make sure the schema file is saved before running queries.

Migrations not applying: Check that your DATABASE_URL in the environment matches what is in drizzle.config.ts. The config file reads process.env while Astro pages read import.meta.env, so both contexts need the variable set. drizzle-kit migrate applies the SQL files produced by drizzle-kit generate.

Connection pool exhaustion: The postgres driver manages its own connection pool. In serverless environments, set a lower pool size, for example postgres(url, { max: 5 }), to avoid exceeding your database's connection limit.

Slow development workflow: Use drizzle-kit push during development instead of generate plus migrate. It applies schema changes directly to the database and is faster for iterating on a local schema before you commit to versioned migration files.

Official Docs and Examples

Conclusion

Drizzle ORM brings type-safe database access to Astro without the overhead of traditional ORMs. The schema-as-code approach means your TypeScript types always match your database structure, catching errors before they reach production. Combined with Astro's static-by-default rendering and per-route prerender = false opt-in for on-demand pages, you can keep your marketing pages static while serving data-driven pages from the database. Start with a simple schema, use push during development, and switch to proper migrations when you are ready to deploy.

Sources

Package versions were read from the npm registry latest tag and all documentation was checked on 2026-05-29.