Integrate Drizzle with Strapi
Integrate Drizzle ORM with Strapi to add type-safe database operations alongside your headless CMS. Connect Drizzle's lightweight TypeScript-first toolkit to handle complex queries, custom tables, and edge deployments while Strapi manages content types and admin functionality
These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
What Is Drizzle ORM?
Drizzle ORM is a lightweight, TypeScript-first database toolkit designed with a SQL-first philosophy. With a 7.4KB minified+gzipped footprint and zero runtime dependencies, it functions as a thin type-safe layer over database drivers rather than a heavy abstraction framework.
Schema definitions live directly in TypeScript files, eliminating code generation steps and providing SQL-like query syntax with full type inference. Drizzle supports PostgreSQL, MySQL, SQLite, and their serverless variants, including Neon, PlanetScale, and Cloudflare D1.
The minimal footprint and fast cold start characteristics make it particularly effective in edge computing scenarios where traditional ORMs introduce unacceptable overhead.
Why Integrate Drizzle ORM with Strapi?
Integrating Drizzle ORM with Strapi brings several key advantages for developers building type-safe, performant applications.
- Type Safety: Drizzle's compile-time type inference automatically generates TypeScript types for all database operations. Your IDE catches type errors during development rather than at runtime, eliminating manual type definitions.
- Query Flexibility: Complex joins, aggregations, and subqueries that are difficult through Strapi's Entity Service API become straightforward with Drizzle's SQL-like builder syntax. Multi-table operations with complex conditions provide the control necessary for sophisticated database operations while maintaining full TypeScript type safety.
- Migration Management: Drizzle Kit auto-generates migration files by comparing schema changes against the database state, reducing boilerplate and human error. While Strapi's migration system is experimental and primarily designed for internal framework use, Drizzle provides production-ready migration tooling for custom tables.
- Performance Benefits: The lightweight architecture delivers measurable results in serverless and edge deployments. Smaller bundle sizes mean faster cold starts, reduced memory footprint, and better performance for applications deployed to edge computing environments such as serverless platforms.
How to Integrate Drizzle ORM with Strapi
Drizzle ORM integrates as a complementary database layer for custom queries and type-safe operations—it cannot replace Strapi's core ORM.
This integration requires installing Drizzle alongside Strapi's existing Knex.js-based system, configuring database connections, defining custom schemas, and implementing a repository pattern within Strapi's controller-service architecture.
Understanding Architectural Boundaries
Before implementing, recognize the fundamental constraints. According to Strapi's database configuration documentation, Strapi can connect to external or existing databases, including those not created by Strapi itself, provided the schema matches the Strapi project schema, and the official documentation does not explicitly address or endorse replacing its core ORM.
Here's the thing: you're running two ORMs side by side. That sounds messy—and it can be if you're not clear about boundaries. But when you treat Strapi as your content management layer and Drizzle as your custom query layer, each system stays in its lane. Strapi's default system handles content types and admin panel operations, while Drizzle manages custom business logic and complex queries.
Database compatibility is limited to PostgreSQL, MySQL/MariaDB, and SQLite—the same databases Strapi 5 supports.
Step 1: Installing Dependencies
For PostgreSQL implementations:
npm i drizzle-orm pg dotenv
npm i -D drizzle-kit tsx @types/pgFor MySQL/MariaDB implementations:
npm i drizzle-orm mysql2 dotenv
npm i -D drizzle-kit tsxThese give you Drizzle's core library, the database driver, and development tools for migrations and TypeScript execution.
Step 2: Setting Up Your Project Structure
Organize your database files alongside Strapi's existing structure:
<project root>
├── drizzle/ # Generated migration files
├── src/
│ ├── api/ # Strapi's default structure
│ └── db/
│ ├── schema.ts # Drizzle schema definitions
│ └── index.ts # Database connection configuration
├── .env
├── drizzle.config.ts
└── package.jsonThis structure maintains clear boundaries between Drizzle's components and Strapi's native architecture.
Step 3: Creating the Configuration File
The configuration file lives in your project root as drizzle.config.ts:
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "postgresql", // or "mysql" for MySQL/MariaDB
schema: "./src/db/schema.ts",
out: "./drizzle",
});The dialect property specifies your database type, schema points to schema definitions, and out designates the migration output directory.
Step 4: Implementing the Database Connection
For PostgreSQL, set up src/db/index.ts with the following connection logic:
import 'dotenv/config';
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
export const db = drizzle(pool);For MySQL/MariaDB implementations:
import 'dotenv/config';
import { createPool } from 'mysql2';
import { drizzle } from 'drizzle-orm/mysql2';
const pool = createPool(process.env.DATABASE_URL!);
export const db = drizzle(pool);Both PostgreSQL and MySQL implementations use connection pooling to manage database connections efficiently. According to Drizzle's documentation, connection pooling settings must be carefully configured and coordinated between Strapi's built-in ORM and Drizzle to avoid connection exhaustion when running dual ORM systems.
One thing that'll come back to haunt you if you're not careful: connection pooling. Both Strapi and Drizzle manage separate pools from the same connection limit—configure those pool.min and pool.max settings thoughtfully or you'll hit database connection limits in production.
Step 5: Defining Type-Safe Schemas
Define your tables with type-safe schemas in src/db/schema.ts:
import { pgTable, integer, varchar, timestamp } from 'drizzle-orm/pg-core';
export const usersTable = pgTable('users', {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
name: varchar({ length: 255 }).notNull(),
email: varchar({ length: 255 }).notNull().unique(),
createdAt: timestamp().defaultNow().notNull(),
});For MySQL implementations, import from drizzle-orm/mysql-core and use mysqlTable with int instead of integer.
Step 6: Implementing the Repository Pattern
Set up a repository pattern by creating src/api/[content-type]/repositories/user.repository.ts:
import { eq } from 'drizzle-orm';
import { db } from '../../../db';
import { usersTable } from '../../../db/schema';
export class UserRepository {
async findById(userId: number) {
const [user] = await db
.select()
.from(usersTable)
.where(eq(usersTable.id, userId));
return user;
}
async findAll() {
return await db.select().from(usersTable);
}
async create(data: { name: string; email: string }) {
const [user] = await db
.insert(usersTable)
.values(data)
.returning();
return user;
}
async update(userId: number, data: Partial<{ name: string; email: string }>) {
const [user] = await db
.update(usersTable)
.set(data)
.where(eq(usersTable.id, userId))
.returning();
return user;
}
async delete(userId: number) {
await db
.delete(usersTable)
.where(eq(usersTable.id, userId));
}
}This repository encapsulates all Drizzle database operations, providing clean abstraction that separates database logic from business logic.
Step 7: Building the Custom Service Layer
Your service layer sits between controllers and repositories. Implement it in src/api/[content-type]/services/user.service.ts:
import { UserRepository } from '../repositories/user.repository';
export default {
async getUser(userId: number) {
const repository = new UserRepository();
return await repository.findById(userId);
},
async getAllUsers() {
const repository = new UserRepository();
return await repository.findAll();
},
async createUser(data: { name: string; email: string }) {
const repository = new UserRepository();
return await repository.create(data);
},
async updateUser(userId: number, data: Partial<{ name: string; email: string }>) {
const repository = new UserRepository();
return await repository.update(userId, data);
},
async deleteUser(userId: number) {
const repository = new UserRepository();
await repository.delete(userId);
},
};This service layer provides the interface that controllers consume, maintaining Strapi's architectural conventions while leveraging Drizzle's type safety.
Step 8: Creating Custom Controllers
Controllers handle HTTP requests and call your services. The implementation in src/api/[content-type]/controllers/user.controller.ts looks like this:
export default {
async find(ctx) {
try {
const users = await strapi.service('api::user.user').getAllUsers();
ctx.body = users;
} catch (err) {
ctx.throw(500, err);
}
},
async findOne(ctx) {
const { id } = ctx.params;
try {
const user = await strapi.service('api::user.user').getUser(Number(id));
ctx.body = user;
} catch (err) {
ctx.throw(500, err);
}
},
async create(ctx) {
const { name, email } = ctx.request.body;
try {
const user = await strapi.service('api::user.user').createUser({ name, email });
ctx.body = user;
} catch (err) {
ctx.throw(500, err);
}
},
async update(ctx) {
const { id } = ctx.params;
const { name, email } = ctx.request.body;
try {
const user = await strapi.service('api::user.user').updateUser(Number(id), { name, email });
ctx.body = user;
} catch (err) {
ctx.throw(500, err);
}
},
async delete(ctx) {
const { id } = ctx.params;
try {
await strapi.service('api::user.user').deleteUser(Number(id));
ctx.body = { message: 'User deleted successfully' };
} catch (err) {
ctx.throw(500, err);
}
},
};These controllers follow Strapi's Koa.js-based request handling pattern, integrating seamlessly with Strapi's routing system while executing Drizzle queries through the service and repository layers.
Step 9: Managing Migrations
Generate migration files based on schema changes:
npx drizzle-kit generateApply migrations to the database:
npx drizzle-kit pushThe generate command creates SQL migration files by comparing the current Drizzle schema snapshot with the most recent snapshot from previous migrations, not by comparing against the current database state. The push command pushes schema changes directly to the configured database without generating migration files.
Step 10: Configuring Environment Variables
Add to .env:
# For PostgreSQL
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
# For MySQL
DATABASE_URL=mysql://username:password@localhost:3306/database_nameThese connection strings should point to the same database Strapi uses, or to a separate database if implementing a multi-database architecture.
Production Considerations
The integration operates within specific boundaries defined by Strapi's architecture. Strapi does not officially support replacing its ORM—external ORMs like Drizzle can be integrated by managing connections and queries outside the default ORM, but this requires custom development effort and is not officially supported.
Connection pooling settings must be compatible with your deployment environment to avoid connection exhaustion when running dual ORM systems. Migration coordination requires managing migrations separately between Strapi's native system and Drizzle's migration toolkit to avoid conflicts. Maintain Strapi's default ORM for content type management to preserve admin panel functionality, permissions, and Strapi-specific features.
Project Example: Custom User Profiles with Type Safety
This example demonstrates integrating Drizzle ORM alongside Strapi for custom user profile data—Strapi manages content types through its admin panel, while Drizzle handles a custom user profiles table with additional fields not managed by Strapi's default user system.
Schema Definition
Using the pattern from Step 5, define a custom profiles table in src/db/schema.ts:
import { pgTable, serial, varchar, text, timestamp } from 'drizzle-orm/pg-core';
export const userProfiles = pgTable('user_profiles', {
id: serial('id').primaryKey(),
userId: varchar('user_id', { length: 255 }).notNull().unique(),
bio: text('bio'),
website: varchar('website', { length: 500 }),
location: varchar('location', { length: 255 }),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});Repository Implementation
Following Step 6's repository pattern in src/api/profiles/repositories/profile.repository.ts:
import { eq } from 'drizzle-orm';
import { db } from '../../../db';
import { userProfiles } from '../../../db/schema';
export class ProfileRepository {
async findByUserId(userId: string) {
const [profile] = await db
.select()
.from(userProfiles)
.where(eq(userProfiles.userId, userId));
return profile;
}
async create(data: { userId: string; bio?: string; website?: string; location?: string }) {
const [profile] = await db
.insert(userProfiles)
.values(data)
.returning();
return profile;
}
async update(userId: string, data: Partial<{ bio: string; website: string; location: string }>) {
const [profile] = await db
.update(userProfiles)
.set({ ...data, updatedAt: new Date() })
.where(eq(userProfiles.userId, userId))
.returning();
return profile;
}
}Service Layer
Implementing Step 7's service pattern in src/api/profiles/services/profile.service.ts:
import { ProfileRepository } from '../repositories/profile.repository';
export default {
async getProfile(userId: string) {
const repository = new ProfileRepository();
return await repository.findByUserId(userId);
},
async createProfile(data: { userId: string; bio?: string; website?: string; location?: string }) {
const repository = new ProfileRepository();
return await repository.create(data);
},
async updateProfile(userId: string, data: Partial<{ bio: string; website: string; location: string }>) {
const repository = new ProfileRepository();
return await repository.update(userId, data);
},
};Controller Implementation
Following Step 8's controller pattern in src/api/profiles/controllers/profile.controller.ts:
export default {
async find(ctx) {
const userId = ctx.state.user?.id;
if (!userId) {
return ctx.throw(401, 'Unauthorized');
}
try {
const profile = await strapi.service('api::profiles.profile').getProfile(userId);
ctx.body = profile;
} catch (err) {
ctx.throw(500, err);
}
},
async create(ctx) {
const userId = ctx.state.user?.id;
if (!userId) {
return ctx.throw(401, 'Unauthorized');
}
const { bio, website, location } = ctx.request.body;
try {
const profile = await strapi.service('api::profiles.profile').createProfile({
userId,
bio,
website,
location,
});
ctx.body = profile;
} catch (err) {
ctx.throw(500, err);
}
},
async update(ctx) {
const userId = ctx.state.user?.id;
if (!userId) {
return ctx.throw(401, 'Unauthorized');
}
const { bio, website, location } = ctx.request.body;
try {
const profile = await strapi.service('api::profiles.profile').updateProfile(userId, {
bio,
website,
location,
});
ctx.body = profile;
} catch (err) {
ctx.throw(500, err);
}
},
};Why This Pattern Works
Strapi manages the core user authentication and content management through its admin panel—Draft & Publish works as expected, permissions control content access, and the Media Library handles images.
Drizzle manages custom database operations for the user profiles table with full TypeScript type safety. The type-safe queries prevent runtime errors, TypeScript inference provides autocomplete in your IDE, and the SQL-like syntax gives you direct control over query execution.
This separation means you're not fighting either system—you're using each for what it does best.
Strapi Open Office Hours
If you have any questions about Strapi 5 or just would like to stop by and say hi, you can join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord Open Office Hours.
For more details, visit the Strapi documentation and Drizzle ORM documentation.
Get Started in Minutes
npx create-strapi-app@latest in your terminal and follow our Quick Start Guide to build your first Strapi project.