Skip to content

Next.js Overview ​

PGRestify provides comprehensive Next.js integration with full support for both App Router and Pages Router architectures. This overview covers the complete Next.js ecosystem integration, performance optimizations, and best practices.

Overview ​

PGRestify's Next.js adapter offers:

  • Universal Router Support: Works with both App Router (13+) and Pages Router
  • Server-Side Rendering: Optimized SSR with automatic hydration
  • Static Site Generation: Pre-render pages at build time with ISR support
  • API Routes: Type-safe API endpoints with built-in validation
  • Authentication: Seamless auth integration with session management
  • Caching: Intelligent caching strategies for optimal performance
  • Edge Runtime: Compatible with Vercel Edge Runtime and middleware

Architecture Support ​

App Router (Next.js 13+) ​

Modern React Server Components architecture with streaming and concurrent features:

typescript
// app/layout.tsx
import { PGRestifyProvider } from '@webcoded/pgrestify/nextjs';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <PGRestifyProvider>
          {children}
        </PGRestifyProvider>
      </body>
    </html>
  );
}

// app/posts/page.tsx
import { createServerClient } from '@webcoded/pgrestify/nextjs';

export default async function PostsPage() {
  const client = createServerClient();
  const { data: posts } = await client
    .from('posts')
    .select('*')
    .order('created_at', { ascending: false });

  return (
    <div>
      <h1>Latest Posts</h1>
      {posts?.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  );
}

Pages Router (Legacy) ​

Traditional Next.js architecture with proven stability:

typescript
// pages/_app.tsx
import { PGRestifyProvider } from '@webcoded/pgrestify/nextjs';
import { AppProps } from 'next/app';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <PGRestifyProvider>
      <Component {...pageProps} />
    </PGRestifyProvider>
  );
}

// pages/posts.tsx
import { createGetServerSideProps } from '@webcoded/pgrestify/nextjs';

export default function PostsPage({ posts }: { posts: Post[] }) {
  return (
    <div>
      <h1>Latest Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  );
}

export const getServerSideProps = createGetServerSideProps(async ({ client }) => {
  const { data: posts } = await client
    .from('posts')
    .select('*')
    .order('created_at', { ascending: false });

  return {
    props: { posts: posts || [] }
  };
});

Client Configuration ​

Environment Detection ​

PGRestify automatically detects your Next.js environment and optimizes accordingly:

typescript
// lib/client.ts
import { createNextJSClient } from '@webcoded/pgrestify/nextjs';

export const client = createNextJSClient({
  url: process.env.NEXT_PUBLIC_POSTGREST_URL!,
  // Automatically detects App Router vs Pages Router
  router: 'auto',
  // Next.js specific optimizations
  nextjs: {
    cache: true,
    edge: false, // Set to true for Edge Runtime
    streaming: true,
    revalidate: ['posts', 'users']
  }
});

Server vs Client Optimization ​

Separate configurations for server and client environments:

typescript
// lib/server-client.ts
import { createServerClient } from '@webcoded/pgrestify/nextjs';

export const serverClient = createServerClient({
  url: process.env.POSTGREST_URL!, // Internal URL, no CORS needed
  auth: {
    // Server-side auth with service keys
    serviceKey: process.env.POSTGREST_SERVICE_KEY!
  },
  cache: {
    enabled: true,
    ttl: 3600 // 1 hour cache for server
  }
});

// lib/client-client.ts  
import { createClientClient } from '@webcoded/pgrestify/nextjs';

export const browserClient = createClientClient({
  url: process.env.NEXT_PUBLIC_POSTGREST_URL!, // Public URL
  auth: {
    persistSession: true,
    autoRefreshToken: true,
    detectSessionInUrl: true
  },
  cache: {
    enabled: true,
    ttl: 300 // 5 minutes cache for browser
  }
});

Data Fetching Patterns ​

Server Components (App Router) ​

Fetch data directly in React Server Components:

typescript
// app/users/[id]/page.tsx
import { createServerClient } from '@webcoded/pgrestify/nextjs';
import { notFound } from 'next/navigation';

interface UserPageProps {
  params: { id: string };
}

export default async function UserPage({ params }: UserPageProps) {
  const client = createServerClient();
  
  const { data: user, error } = await client
    .from('users')
    .select(`
      *,
      posts (
        id,
        title,
        created_at
      )
    `)
    .eq('id', params.id)
    .single();

  if (error || !user) {
    notFound();
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      
      <h2>Recent Posts</h2>
      {user.posts?.map(post => (
        <article key={post.id}>
          <h3>{post.title}</h3>
          <time>{new Date(post.created_at).toLocaleDateString()}</time>
        </article>
      ))}
    </div>
  );
}

// Generate metadata
export async function generateMetadata({ params }: UserPageProps) {
  const client = createServerClient();
  const { data: user } = await client
    .from('users')
    .select('name, bio')
    .eq('id', params.id)
    .single();

  return {
    title: `${user?.name} - User Profile`,
    description: user?.bio || `Profile page for ${user?.name}`
  };
}

Client Components (App Router) ​

Interactive components with client-side data fetching:

typescript
'use client';

import { useQuery, useMutation } from '@webcoded/pgrestify/nextjs';
import { useState } from 'react';

export default function PostForm() {
  const [title, setTitle] = useState('');
  const [content, setContent] = useState('');

  const { data: categories, isLoading } = useQuery({
    queryKey: ['categories'],
    queryFn: ({ client }) => client.from('categories').select('*')
  });

  const createPost = useMutation({
    mutationFn: ({ client, data }) => 
      client.from('posts').insert(data).select().single(),
    onSuccess: () => {
      setTitle('');
      setContent('');
    }
  });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    await createPost.mutateAsync({
      title,
      content,
      published: true
    });
  };

  if (isLoading) return <div>Loading categories...</div>;

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
        placeholder="Post title"
        required
      />
      
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="Post content"
        required
      />
      
      <select>
        {categories?.map(category => (
          <option key={category.id} value={category.id}>
            {category.name}
          </option>
        ))}
      </select>
      
      <button type="submit" disabled={createPost.isPending}>
        {createPost.isPending ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  );
}

Performance Optimizations ​

Automatic Caching ​

PGRestify provides intelligent caching optimized for Next.js:

typescript
// Automatic cache strategy based on router type
const client = createNextJSClient({
  url: process.env.NEXT_PUBLIC_POSTGREST_URL!,
  nextjs: {
    cache: true, // Enables automatic caching
    revalidate: [
      'posts',    // Tag for posts
      'users',    // Tag for users  
      'comments'  // Tag for comments
    ]
  }
});

// Manual cache control
import { revalidateTag } from '@webcoded/pgrestify/nextjs';

// In a Server Action or API route
export async function updatePost(id: string, data: Partial<Post>) {
  const client = createServerClient();
  
  const { data: post } = await client
    .from('posts')
    .update(data)
    .eq('id', id)
    .select()
    .single();

  // Revalidate related caches
  revalidateTag('posts');
  revalidateTag(`post-${id}`);
  
  return post;
}

Edge Runtime Support ​

Compatible with Vercel Edge Runtime for global performance:

typescript
// middleware.ts
import { createAuthMiddleware } from '@webcoded/pgrestify/nextjs';

export const middleware = createAuthMiddleware({
  protectedPaths: ['/dashboard', '/profile'],
  publicPaths: ['/', '/login', '/signup'],
  signInUrl: '/login'
});

export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*']
};

// Edge API route
// app/api/posts/route.ts
import { createRouteHandler } from '@webcoded/pgrestify/nextjs';

export const runtime = 'edge';

export const { GET, POST } = createRouteHandler({
  GET: async ({ client, request }) => {
    const { searchParams } = new URL(request.url);
    const category = searchParams.get('category');

    let query = client.from('posts').select('*');
    
    if (category) {
      query = query.eq('category', category);
    }

    const { data: posts } = await query;
    
    return Response.json(posts);
  },
  
  POST: async ({ client, request }) => {
    const body = await request.json();
    
    const { data: post, error } = await client
      .from('posts')
      .insert(body)
      .select()
      .single();

    if (error) {
      return Response.json({ error: error.message }, { status: 400 });
    }

    return Response.json(post, { status: 201 });
  }
});

Authentication Integration ​

Session Management ​

Seamless authentication with Next.js auth patterns:

typescript
// lib/auth.ts
import { createAuthClient } from '@webcoded/pgrestify/nextjs';

export const auth = createAuthClient({
  url: process.env.NEXT_PUBLIC_POSTGREST_URL!,
  session: {
    persistSession: true,
    autoRefreshToken: true,
    detectSessionInUrl: true
  }
});

// app/login/page.tsx
'use client';

import { useRouter } from 'next/navigation';
import { useSession } from '@webcoded/pgrestify/nextjs';

export default function LoginPage() {
  const router = useRouter();
  const { signIn, isLoading } = useSession();

  const handleLogin = async (email: string, password: string) => {
    try {
      await signIn({ email, password });
      router.push('/dashboard');
    } catch (error) {
      console.error('Login failed:', error);
    }
  };

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      const formData = new FormData(e.currentTarget);
      handleLogin(
        formData.get('email') as string,
        formData.get('password') as string
      );
    }}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      <button type="submit" disabled={isLoading}>
        {isLoading ? 'Signing in...' : 'Sign In'}
      </button>
    </form>
  );
}

Protected Routes ​

Implement route protection with middleware:

typescript
// middleware.ts
import { createAuthMiddleware } from '@webcoded/pgrestify/nextjs';

export const middleware = createAuthMiddleware({
  protectedPaths: ['/dashboard/:path*', '/profile/:path*'],
  publicPaths: ['/', '/about', '/contact'],
  signInUrl: '/login',
  validateAuth: async (request) => {
    // Custom auth validation logic
    const token = request.cookies.get('auth-token');
    if (!token) return false;
    
    // Validate token with your auth service
    return validateToken(token.value);
  }
});

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
};

Deployment Considerations ​

Environment Variables ​

Configure environment variables for different deployment targets:

bash
# .env.local (development)
NEXT_PUBLIC_POSTGREST_URL=http://localhost:3000
POSTGREST_URL=http://localhost:3000
POSTGREST_SERVICE_KEY=your_service_key
DATABASE_URL=postgresql://user:pass@localhost:5432/db

# .env.production
NEXT_PUBLIC_POSTGREST_URL=https://api.yourdomain.com
POSTGREST_URL=https://internal-api.yourdomain.com
POSTGREST_SERVICE_KEY=your_production_service_key
DATABASE_URL=your_production_database_url

Vercel Deployment ​

Optimal configuration for Vercel deployment:

json
// vercel.json
{
  "build": {
    "env": {
      "ENABLE_EXPERIMENTAL_COREPACK": "1"
    }
  },
  "functions": {
    "app/api/**": {
      "maxDuration": 30
    }
  },
  "crons": [
    {
      "path": "/api/cron/cleanup",
      "schedule": "0 2 * * *"
    }
  ]
}

Docker Configuration ​

Docker setup for containerized deployment:

dockerfile
# Dockerfile
FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm ci --only=production

# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NEXT_TELEMETRY_DISABLED 1

RUN npm run build

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]

Best Practices ​

Project Structure ​

Recommended folder structure for Next.js with PGRestify:

app/                        # App Router (Next.js 13+)
├── api/                   # API routes
│   ├── auth/             # Authentication endpoints
│   ├── posts/            # Posts API
│   └── users/            # Users API
├── dashboard/            # Protected dashboard
├── (auth)/               # Auth route group
│   ├── login/
│   └── signup/
└── layout.tsx            # Root layout

lib/                       # Shared utilities
├── client.ts             # PGRestify client setup
├── auth.ts               # Authentication logic
├── utils.ts              # Helper functions
└── types.ts              # TypeScript types

components/               # Reusable components
├── ui/                   # Basic UI components
├── forms/                # Form components
└── layouts/              # Layout components

hooks/                    # Custom hooks
├── useAuth.ts           # Authentication hook
├── usePosts.ts          # Posts-specific hooks
└── useUsers.ts          # User-specific hooks

Error Handling ​

Comprehensive error handling across your Next.js app:

typescript
// lib/error-handler.ts
export class PGRestifyError extends Error {
  constructor(
    message: string,
    public code: string,
    public details?: any
  ) {
    super(message);
    this.name = 'PGRestifyError';
  }
}

export function handleAPIError(error: any) {
  if (error.code) {
    switch (error.code) {
      case 'PGRST116':
        throw new PGRestifyError('Resource not found', '404', error);
      case 'PGRST204':
        throw new PGRestifyError('Unauthorized', '401', error);
      default:
        throw new PGRestifyError('Server error', '500', error);
    }
  }
  
  throw new PGRestifyError('Unknown error', '500', error);
}

// app/error.tsx (App Router error boundary)
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Performance Tips ​

  1. Use Server Components: Fetch data on the server when possible
  2. Implement Proper Caching: Use Next.js cache and PGRestify cache strategies
  3. Optimize Bundle Size: Import only what you need
  4. Use Streaming: Enable streaming for better UX
  5. Monitor Performance: Use Next.js analytics and monitoring

Migration Guide ​

From Pages to App Router ​

Migrating existing PGRestify Pages Router apps to App Router:

typescript
// Before (Pages Router)
// pages/posts.tsx
export const getServerSideProps = createGetServerSideProps(async ({ client }) => {
  const { data: posts } = await client.from('posts').select('*');
  return { props: { posts } };
});

// After (App Router)
// app/posts/page.tsx
export default async function PostsPage() {
  const client = createServerClient();
  const { data: posts } = await client.from('posts').select('*');
  
  return <PostList posts={posts} />;
}

Next Steps ​

Released under the MIT License.