Files
gh-neondatabase-labs-ai-rul…/skills/neon-drizzle/guides/existing-project.md
2025-11-30 08:43:11 +08:00

11 KiB

Existing Project Integration

Complete Walkthrough: This is a self-contained, step-by-step guide with its own numbered phases (Phase 1-8). Follow each phase in order to safely add Drizzle to your existing application.

Guide for adding Drizzle ORM to an existing application with Neon.

Important:

  • Remember to run the neon-plugin:add-neon-docs skill with the parameter SKILL_NAME="neon-drizzle" after completing the guide.

Table of Contents


Workflow Checklist

When following this guide, I will track these high-level tasks:

  • Pre-integration check (detect existing ORMs, database schema, environment)
  • Install Drizzle dependencies without disrupting existing setup
  • Create isolated Drizzle configuration (separate from existing code)
  • Choose and implement schema strategy (new tables vs mirroring existing)
  • Handle migrations safely based on schema strategy
  • Set up coexistence patterns and gradual migration approach
  • Verify Drizzle integration without breaking existing functionality
  • Add Neon Drizzle best practices to project docs

Phase 1: Pre-Integration Check

Before adding Drizzle, check for conflicts:

1.1. Check for Other ORMs

grep -E '"(prisma|typeorm|sequelize|mongoose)"' package.json

If found:

  • Consider migration strategy (coexistence vs replacement)
  • Document which tables use which ORM
  • Plan gradual migration if needed

1.2. Check Database Schema

Connect to your database and verify existing tables:

psql $DATABASE_URL -c "\dt"

Important: Note existing tables - Drizzle should not conflict with them.

1.3. Check Environment Setup

ls .env .env.local .env.production
grep DATABASE_URL .env*

If DATABASE_URL exists:

  • Verify connection string format is compatible with Neon (postgresql://...)
  • If it's a different database provider, you'll need to migrate or provision a Neon database

If DATABASE_URL does NOT exist: Follow the database provisioning steps from guides/new-project.md Phase 3.1:

  1. List the projects using the neon MCP Server to check existing projects
  2. Create a new project using the neon MCP Server if needed
  3. Get the connection string using the neon MCP Server
  4. Write to appropriate environment file (.env.local for Next.js, .env for others)
  5. Add environment file to .gitignore

Phase 2: Incremental Installation

Add Drizzle without disrupting existing setup:

2.1. Install Dependencies

For Vercel/Edge:

[package-manager] add drizzle-orm @neondatabase/serverless
[package-manager] add -D drizzle-kit dotenv

For Node.js:

[package-manager] add drizzle-orm @neondatabase/serverless ws
[package-manager] add -D drizzle-kit dotenv @types/ws

2.2. Create Isolated Drizzle Directory

Keep Drizzle separate from existing code:

mkdir -p src/drizzle

Structure:

src/drizzle/
├── index.ts      # Connection
├── schema.ts     # New schemas only
└── migrations/   # Drizzle migrations

Phase 3: Configuration

3.1. Create Drizzle Config

Create drizzle.config.ts with explicit environment loading:

CRITICAL: The config({ path: '...' }) must match your environment file name.

For Next.js (using .env.local):

import { defineConfig } from 'drizzle-kit';
import { config } from 'dotenv';

// Load .env.local explicitly
config({ path: '.env.local' });

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

For other projects (using .env):

import { defineConfig } from 'drizzle-kit';
import { config } from 'dotenv';

// Load .env explicitly
config({ path: '.env' });

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

Notes:

  • Point schema and migrations to src/drizzle/ to avoid conflicts with existing code
  • Explicit dotenv path prevents "url: undefined" errors during migrations

3.2. Create Connection

src/drizzle/index.ts - Choose adapter based on environment (see references/adapters.md):

HTTP (Vercel/Edge):

import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';

const sql = neon(process.env.DATABASE_URL!);
export const drizzleDb = drizzle(sql);

WebSocket (Node.js):

import { drizzle } from 'drizzle-orm/neon-serverless';
import { Pool, neonConfig } from '@neondatabase/serverless';
import ws from 'ws';

neonConfig.webSocketConstructor = ws;
const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
export const drizzleDb = drizzle(pool);

Important: Name export as drizzleDb to avoid conflicts with existing db exports.

Phase 4: Schema Strategy

Choose integration approach:

4.1. Option A: New Tables Only

Create schemas for new features only, leave existing tables alone:

src/drizzle/schema.ts:

import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';

export const newFeatureTable = pgTable('new_feature', {
  id: serial('id').primaryKey(),
  data: text('data').notNull(),
  createdAt: timestamp('created_at').defaultNow(),
});

Pros:

  • No migration of existing data
  • Zero risk to current functionality
  • Gradual adoption

Cons:

  • Mixed query patterns (Drizzle + existing ORM)
  • Two connection patterns in codebase

4.2. Option B: Mirror Existing Tables

Define schemas for existing tables to gradually migrate queries:

import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core';

export const existingUsers = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull(),
  name: varchar('name', { length: 255 }),
  createdAt: timestamp('created_at'),
});

Pros:

  • Can query existing data with Drizzle
  • Gradually replace old ORM queries
  • Type-safe access to existing tables

Cons:

  • Must match existing schema exactly
  • Requires careful migration strategy
  1. Start with Option A (new tables only)
  2. Once comfortable, add schemas for frequently-queried existing tables (Option B)
  3. Gradually migrate queries from old ORM to Drizzle
  4. Eventually remove old ORM

Phase 5: Migration Handling

5.1. For New Tables

Generate and run migrations normally:

[package-manager] drizzle-kit generate
export DATABASE_URL="$(grep DATABASE_URL .env.local | cut -d '=' -f2)" && \
[package-manager] drizzle-kit migrate

5.2. For Existing Tables

Do NOT run migrations - tables already exist!

Instead, use Drizzle schemas for querying only:

import { drizzleDb } from './drizzle';
import { existingUsers } from './drizzle/schema';

const users = await drizzleDb.select().from(existingUsers);

5.3. Mixed Scenario

If you have both new and existing tables:

  1. Define all schemas in schema.ts
  2. Run drizzle-kit generate
  3. Manually edit generated migration to remove SQL for existing tables
  4. Apply migration

See references/migrations.md for advanced patterns.

5.4. Add Migration Scripts

Add these convenience scripts to your package.json:

{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:push": "drizzle-kit push",
    "db:studio": "drizzle-kit studio"
  }
}

Usage:

npm run db:generate  # Generate migrations from schema changes
npm run db:migrate   # Apply pending migrations
npm run db:push      # Push schema directly (dev only)
npm run db:studio    # Open Drizzle Studio

Note: Replace npm run with your package manager's equivalent (pnpm, yarn, bun).

Phase 6: Coexistence Patterns

6.1. Naming Conventions

Keep clear separation:

import { db as prismaDb } from './lib/prisma';
import { drizzleDb } from './drizzle';

const prismaUsers = await prismaDb.user.findMany();
const drizzleFeatures = await drizzleDb.select().from(newFeatureTable);

6.2. Gradual Migration

Step 1: New features use Drizzle

async function createFeature(data: NewFeatureInput) {
  return drizzleDb.insert(newFeatureTable).values(data).returning();
}

Step 2: Migrate read queries (safe, no data changes)

async function getUsers() {
  return drizzleDb.select().from(existingUsers);
}

Step 3: Migrate write queries (after thorough testing)

async function updateUser(id: number, data: UserUpdate) {
  return drizzleDb.update(existingUsers)
    .set(data)
    .where(eq(existingUsers.id, id));
}

Step 4: Remove old ORM once all queries migrated

Phase 7: Verification

Test integration without breaking existing functionality:

7.1. Test New Tables

import { drizzleDb } from './drizzle';
import { newFeatureTable } from './drizzle/schema';

const result = await drizzleDb.insert(newFeatureTable)
  .values({ data: 'test' })
  .returning();

console.log('New table works:', result);

7.2. Test Existing Tables (if mirrored)

import { drizzleDb } from './drizzle';
import { existingUsers } from './drizzle/schema';

const users = await drizzleDb.select().from(existingUsers);
console.log('Existing table accessible:', users);

7.3. Verify Old ORM Still Works

import { db as oldDb } from './lib/your-orm';

const oldQuery = await oldDb.users.findMany();
console.log('Old ORM still works:', oldQuery);

Phase 8: Add Best Practices References

Before executing the add-neon-docs skill, provide a summary of everything that has been done:

" ... Drizzle integration is complete! Now adding documentation references..."

Then execute the neon-plugin:add-neon-docs skill with the parameter SKILL_NAME="neon-drizzle"

This will add reference links to Neon + Drizzle best practices documentation in your project's AI documentation file, helping AI assistants provide better guidance in future conversations.


Integration Complete!

Your Drizzle integration with the existing project is ready to use.