Skip to content

Next.js Migration Guide

Complete guide for migrating your Next.js application to PGRestify from other data fetching solutions, including Supabase, Prisma, and direct PostgREST implementations.

Overview

This guide covers migration strategies for:

  • Direct PostgREST/fetch implementations
  • Supabase client libraries
  • Prisma ORM
  • Other PostgreSQL clients
  • Pages Router to App Router migration

Migration from Direct PostgREST

Before: Direct Fetch Calls

typescript
// Before: Direct fetch with PostgREST
async function getUsers() {
  const response = await fetch(`${POSTGREST_URL}/users?select=*`, {
    headers: {
      'apikey': ANON_KEY,
      'Authorization': `Bearer ${ANON_KEY}`
    }
  })
  
  if (!response.ok) {
    throw new Error('Failed to fetch users')
  }
  
  return response.json()
}

// Complex query with joins
async function getPostsWithAuthors() {
  const url = `${POSTGREST_URL}/posts?select=*,author:users(name,email)&order=created_at.desc&limit=10`
  const response = await fetch(url, {
    headers: {
      'apikey': ANON_KEY,
      'Authorization': `Bearer ${ANON_KEY}`
    }
  })
  
  return response.json()
}

After: PGRestify Client

typescript
// After: PGRestify client
import { createNextJSClient } from '@webcoded/pgrestify/nextjs'

const client = createNextJSClient({
  url: process.env.POSTGREST_URL!,
  auth: {
    anonKey: process.env.ANON_KEY!
  }
})

async function getUsers() {
  const result = await client
    .from('users')
    .select('*')
    .execute()
  
  if (result.error) {
    throw result.error
  }
  
  return result.data
}

// Complex query with type safety
async function getPostsWithAuthors() {
  const result = await client
    .from('posts')
    .select('*, author:users(name, email)')
    .order('created_at', { ascending: false })
    .limit(10)
    .execute()
  
  return result.data
}

Migration from Supabase

Authentication Migration

typescript
// Before: Supabase Auth
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(url, anonKey)

// Sign up
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'password'
})

// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'password'
})

// Get session
const { data: { session } } = await supabase.auth.getSession()
typescript
// After: PGRestify Auth
import { createNextJSClient } from '@webcoded/pgrestify/nextjs'

const client = createNextJSClient({
  url: process.env.POSTGREST_URL!,
  auth: {
    persistSession: true
  }
})

// Sign up
const result = await client.auth.signUp({
  email: 'user@example.com',
  password: 'password'
})

// Sign in
const result = await client.auth.signIn({
  email: 'user@example.com',
  password: 'password'
})

// Get session
const session = await client.auth.getSession()

Database Queries Migration

typescript
// Before: Supabase Database
// Simple query
const { data, error } = await supabase
  .from('users')
  .select('*')

// Insert
const { data, error } = await supabase
  .from('posts')
  .insert({ title: 'New Post', content: 'Content' })
  .select()

// Update
const { data, error } = await supabase
  .from('posts')
  .update({ title: 'Updated' })
  .eq('id', 1)
  .select()

// Delete
const { error } = await supabase
  .from('posts')
  .delete()
  .eq('id', 1)

// RPC
const { data, error } = await supabase
  .rpc('get_user_stats', { user_id: 1 })
typescript
// After: PGRestify
// Simple query
const result = await client
  .from('users')
  .select('*')
  .execute()

// Insert
const result = await client
  .from('posts')
  .insert({ title: 'New Post', content: 'Content' })
  .select('*')
  .execute()

// Update
const result = await client
  .from('posts')
  .update({ title: 'Updated' })
  .eq('id', 1)
  .select('*')
  .execute()

// Delete
const result = await client
  .from('posts')
  .delete()
  .eq('id', 1)
  .execute()

// RPC
const result = await client
  .rpc('get_user_stats', { user_id: 1 })
  .execute()

Real-time Migration

typescript
// Before: Supabase Realtime
const channel = supabase
  .channel('posts')
  .on('postgres_changes', 
    { event: 'INSERT', schema: 'public', table: 'posts' },
    (payload) => {
      console.log('New post:', payload.new)
    }
  )
  .subscribe()

// Cleanup
channel.unsubscribe()
typescript
// After: PGRestify Realtime
const subscription = client
  .from('posts')
  .on('INSERT', (payload) => {
    console.log('New post:', payload.new)
  })
  .subscribe()

// Cleanup
subscription.unsubscribe()

Migration from Prisma

Schema to Types Migration

typescript
// Before: Prisma Schema
// schema.prisma
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}
typescript
// After: PGRestify Types
// Generate types from PostgREST schema
npx pgrestify generate --url http://localhost:3000 --output ./src/types

// Or define manually
interface User {
  id: number
  email: string
  name?: string
  created_at: string
}

interface Post {
  id: number
  title: string
  content?: string
  published: boolean
  author_id: number
}

Query Migration

typescript
// Before: Prisma Queries
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

// Find many
const users = await prisma.user.findMany({
  where: {
    email: { contains: '@example.com' }
  },
  include: {
    posts: true
  },
  orderBy: {
    createdAt: 'desc'
  },
  take: 10,
  skip: 20
})

// Find unique
const user = await prisma.user.findUnique({
  where: { id: 1 }
})

// Create
const user = await prisma.user.create({
  data: {
    email: 'new@example.com',
    name: 'New User'
  }
})

// Update
const user = await prisma.user.update({
  where: { id: 1 },
  data: { name: 'Updated Name' }
})

// Delete
await prisma.user.delete({
  where: { id: 1 }
})

// Transactions
const [user, post] = await prisma.$transaction([
  prisma.user.create({ data: userData }),
  prisma.post.create({ data: postData })
])
typescript
// After: PGRestify Queries
import { createNextJSClient } from '@webcoded/pgrestify/nextjs'

const client = createNextJSClient({ url: process.env.POSTGREST_URL! })

// Find many
const result = await client
  .from('users')
  .select('*, posts(*)')
  .ilike('email', '%@example.com%')
  .order('created_at', { ascending: false })
  .range(20, 29) // Skip 20, take 10
  .execute()

// Find unique
const result = await client
  .from('users')
  .select('*')
  .eq('id', 1)
  .single()
  .execute()

// Create
const result = await client
  .from('users')
  .insert({
    email: 'new@example.com',
    name: 'New User'
  })
  .select('*')
  .single()
  .execute()

// Update
const result = await client
  .from('users')
  .update({ name: 'Updated Name' })
  .eq('id', 1)
  .select('*')
  .single()
  .execute()

// Delete
const result = await client
  .from('users')
  .delete()
  .eq('id', 1)
  .execute()

// Transactions (using RPC)
const result = await client
  .rpc('create_user_and_post', {
    user_data: userData,
    post_data: postData
  })
  .execute()

Repository Pattern Migration

typescript
// Before: Prisma Repository Pattern
class UserRepository {
  async findById(id: number) {
    return prisma.user.findUnique({
      where: { id },
      include: { posts: true }
    })
  }
  
  async findByEmail(email: string) {
    return prisma.user.findUnique({
      where: { email }
    })
  }
  
  async create(data: CreateUserDto) {
    return prisma.user.create({ data })
  }
}
typescript
// After: PGRestify Repository Pattern
import { Repository } from '@webcoded/pgrestify'

class UserRepository extends Repository<User> {
  constructor(client: PostgRESTClient) {
    super(client, 'users')
  }
  
  async findById(id: number) {
    return this.findOne({
      filters: { id },
      select: '*, posts(*)'
    })
  }
  
  async findByEmail(email: string) {
    return this.findOne({
      filters: { email }
    })
  }
  
  async create(data: CreateUserDto) {
    return this.insert(data)
  }
}

// Or use the built-in repository
const userRepo = client.getRepository<User>('users')
const user = await userRepo.findOne({ id: 1 })

Pages Router to App Router Migration

Data Fetching Migration

typescript
// Before: Pages Router with getServerSideProps
// pages/users.tsx
export async function getServerSideProps() {
  const result = await client
    .from('users')
    .select('*')
    .execute()
  
  return {
    props: {
      users: result.data || []
    }
  }
}

export default function UsersPage({ users }) {
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  )
}
typescript
// After: App Router with Server Components
// app/users/page.tsx
export default async function UsersPage() {
  const result = await client
    .from('users')
    .select('*')
    .execute()
  
  const users = result.data || []
  
  return (
    <div>
      {users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  )
}

API Routes Migration

typescript
// Before: Pages Router API Route
// pages/api/users.ts
import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'GET') {
    const result = await client.from('users').select('*').execute()
    res.status(200).json(result.data)
  } else {
    res.status(405).json({ error: 'Method not allowed' })
  }
}
typescript
// After: App Router Route Handler
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const result = await client.from('users').select('*').execute()
  return NextResponse.json(result.data)
}

Migration Checklist

Pre-Migration

  • [ ] Audit current data fetching patterns
  • [ ] Identify authentication requirements
  • [ ] Document current API endpoints
  • [ ] Plan migration phases
  • [ ] Set up test environment

Phase 1: Setup

  • [ ] Install PGRestify
  • [ ] Configure environment variables
  • [ ] Set up client instances
  • [ ] Generate TypeScript types
  • [ ] Create test suite

Phase 2: Authentication

  • [ ] Migrate auth configuration
  • [ ] Update sign up/sign in flows
  • [ ] Migrate session management
  • [ ] Update protected routes
  • [ ] Test auth flows

Phase 3: Data Fetching

  • [ ] Replace database queries
  • [ ] Update API routes/handlers
  • [ ] Migrate real-time subscriptions
  • [ ] Update cache strategies
  • [ ] Implement error handling

Phase 4: Testing

  • [ ] Run unit tests
  • [ ] Run integration tests
  • [ ] Performance testing
  • [ ] User acceptance testing
  • [ ] Fix identified issues

Phase 5: Deployment

  • [ ] Update CI/CD pipelines
  • [ ] Configure production environment
  • [ ] Deploy to staging
  • [ ] Monitor for issues
  • [ ] Deploy to production

Common Migration Patterns

Gradual Migration

typescript
// Use adapter pattern for gradual migration
class DataAdapter {
  private pgrestify: PostgRESTClient
  private supabase?: SupabaseClient
  
  constructor() {
    this.pgrestify = createNextJSClient({ /* config */ })
    // Keep old client during migration
    this.supabase = createClient(url, key)
  }
  
  async getUsers() {
    // Use feature flag to switch implementations
    if (process.env.USE_PGRESTIFY === 'true') {
      const result = await this.pgrestify.from('users').select('*').execute()
      return result.data
    } else {
      const { data } = await this.supabase.from('users').select('*')
      return data
    }
  }
}

Parallel Running

typescript
// Run both implementations in parallel for comparison
async function compareImplementations() {
  const [oldResult, newResult] = await Promise.all([
    oldClient.from('users').select('*'),
    newClient.from('users').select('*').execute()
  ])
  
  // Log differences for monitoring
  if (JSON.stringify(oldResult.data) !== JSON.stringify(newResult.data)) {
    console.warn('Implementation mismatch detected')
  }
  
  return newResult.data
}

Troubleshooting

Common Issues

1. Type Mismatches

typescript
// Solution: Generate types from PostgREST schema
npx pgrestify generate --url $POSTGREST_URL --output ./src/types

2. Authentication Errors

typescript
// Solution: Ensure JWT secret matches
const client = createNextJSClient({
  auth: {
    jwtSecret: process.env.JWT_SECRET // Must match PostgREST
  }
})

3. Query Syntax Differences

typescript
// Map old syntax to new
function mapSupabaseQuery(query) {
  // Supabase: .single()
  // PGRestify: .single().execute()
  
  // Supabase: .select('*, users!inner(*)')
  // PGRestify: .select('*, users(*)').innerJoin('users')
}

4. Real-time Connection Issues

typescript
// Ensure WebSocket URL is configured
const client = createNextJSClient({
  realtime: {
    url: process.env.REALTIME_URL || 'ws://localhost:3000'
  }
})

Performance Optimization

After Migration

  1. Enable Query Caching
typescript
const client = createNextJSClient({
  cache: {
    enabled: true,
    ttl: 3600
  }
})
  1. Optimize Bundle Size
typescript
// Import only what you need
import { createClient } from '@webcoded/pgrestify/core'
// Instead of
import { createNextJSClient } from '@webcoded/pgrestify/nextjs'
  1. Use Connection Pooling
typescript
const client = createNextJSClient({
  connection: {
    poolSize: 10,
    keepAlive: true
  }
})

Resources

Documentation

Migration Tools

Support

Next Steps

Released under the MIT License.