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