perf hacks
Edge & APIsAPI Routes

API Route Performance

Optimize Next.js API routes and server functions for faster response times

API route performance directly impacts TTFB and user experience. This guide covers techniques to make your API routes faster and more efficient.

Route Handler Basics

app/api/users/route.ts
export async function GET(request: Request) {
  const users = await getUsers();
  return Response.json(users);
}

Response Caching

Cache-Control Headers

app/api/products/route.ts
export async function GET() {
  const products = await getProducts();

  return Response.json(products, {
    headers: {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
    },
  });
}
DirectiveMeaning
publicCDN can cache
privateBrowser only
s-maxage=60CDN caches for 60s
max-age=60Browser caches for 60s
stale-while-revalidate=300Serve stale while refreshing

Dynamic Caching

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const category = searchParams.get('category');

  const products = await getProductsByCategory(category);

  // Cache based on category
  const cacheTime = category === 'featured' ? 300 : 60;

  return Response.json(products, {
    headers: {
      'Cache-Control': `public, s-maxage=${cacheTime}`,
    },
  });
}

Database Query Optimization

Connection Pooling

lib/db.ts
import { Pool } from 'pg';

// Reuse connection pool across requests
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  idleTimeoutMillis: 30000,
});

export async function query(text: string, params?: any[]) {
  return pool.query(text, params);
}

Avoid N+1 Queries

// ❌ Bad: N+1 queries
export async function GET() {
  const posts = await db.query('SELECT * FROM posts');

  for (const post of posts) {
    post.author = await db.query(
      'SELECT * FROM users WHERE id = $1',
      [post.authorId]
    );
  }

  return Response.json(posts);
}

// ✅ Good: Single query with JOIN
export async function GET() {
  const posts = await db.query(`
    SELECT posts.*, users.name as authorName, users.avatar as authorAvatar
    FROM posts
    JOIN users ON posts.author_id = users.id
    ORDER BY posts.created_at DESC
    LIMIT 50
  `);

  return Response.json(posts);
}

Query Result Caching

import { unstable_cache } from 'next/cache';

const getCachedProducts = unstable_cache(
  async (category: string) => {
    return db.query('SELECT * FROM products WHERE category = $1', [category]);
  },
  ['products'],
  { revalidate: 60 }
);

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const category = searchParams.get('category') || 'all';

  const products = await getCachedProducts(category);
  return Response.json(products);
}

Streaming Responses

Streaming JSON

export async function GET() {
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      controller.enqueue(encoder.encode('['));

      let first = true;
      for await (const item of fetchItemsGenerator()) {
        if (!first) controller.enqueue(encoder.encode(','));
        controller.enqueue(encoder.encode(JSON.stringify(item)));
        first = false;
      }

      controller.enqueue(encoder.encode(']'));
      controller.close();
    },
  });

  return new Response(stream, {
    headers: { 'Content-Type': 'application/json' },
  });
}

Server-Sent Events

export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder();

      const subscription = dataSource.subscribe((data) => {
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
        );
      });

      // Cleanup on close
      return () => subscription.unsubscribe();
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}

Edge Runtime

// Runs at edge for lower latency
export const runtime = 'edge';

export async function GET(request: Request) {
  // Edge-compatible code only
  const data = await fetch('https://api.example.com/data');
  return Response.json(await data.json());
}

Edge Limitations

Not AvailableEdge-Compatible Alternative
fs moduleFetch from object storage (S3, R2)
Traditional SQL pools (pg, mysql2)Edge databases (PlanetScale, Turso, Neon)
Long-running processesKV stores (Vercel KV, Upstash)
Node.js-specific APIsWeb standard APIs (fetch, crypto, Headers)

Compression

Automatic with Vercel

Vercel automatically compresses responses with gzip/brotli.

Manual Compression

import { gzip } from 'zlib';
import { promisify } from 'util';

const gzipAsync = promisify(gzip);

export async function GET() {
  const data = await getLargeDataset();
  const json = JSON.stringify(data);

  const compressed = await gzipAsync(json);

  return new Response(compressed, {
    headers: {
      'Content-Type': 'application/json',
      'Content-Encoding': 'gzip',
    },
  });
}

Request Handling

Parse Body Once

// ❌ Bad: Parsing body multiple times
export async function POST(request: Request) {
  const body1 = await request.json(); // First parse
  validateBody(await request.json()); // Error! Stream already consumed
}

// ✅ Good: Parse once, reuse
export async function POST(request: Request) {
  const body = await request.json();
  validateBody(body);
  processData(body);
}

Early Validation

import { z } from 'zod';

const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
});

export async function POST(request: Request) {
  const body = await request.json();

  // Validate early, fail fast
  const result = CreateUserSchema.safeParse(body);
  if (!result.success) {
    return Response.json(
      { error: result.error.issues },
      { status: 400 }
    );
  }

  // Proceed with validated data
  const user = await createUser(result.data);
  return Response.json(user, { status: 201 });
}

Parallel Processing

export async function GET(request: Request) {
  // ❌ Sequential: 300ms + 200ms + 100ms = 600ms
  const users = await getUsers();
  const products = await getProducts();
  const stats = await getStats();

  // ✅ Parallel: max(300ms, 200ms, 100ms) = 300ms
  const [users, products, stats] = await Promise.all([
    getUsers(),
    getProducts(),
    getStats(),
  ]);

  return Response.json({ users, products, stats });
}

Error Handling

export async function GET(request: Request) {
  try {
    const data = await fetchData();
    return Response.json(data);
  } catch (error) {
    console.error('API Error:', error);

    if (error instanceof DatabaseError) {
      return Response.json(
        { error: 'Database unavailable' },
        { status: 503 }
      );
    }

    return Response.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Monitoring

export async function GET(request: Request) {
  const start = performance.now();

  try {
    const result = await processRequest(request);

    const duration = performance.now() - start;
    console.log(`API response time: ${duration.toFixed(2)}ms`);

    return Response.json(result, {
      headers: {
        'Server-Timing': `total;dur=${duration.toFixed(2)}`,
      },
    });
  } catch (error) {
    const duration = performance.now() - start;
    console.error(`API error after ${duration.toFixed(2)}ms:`, error);
    throw error;
  }
}

Checklist

Caching

Set appropriate Cache-Control headers Tell CDN and browsers how to cache responses.

Use unstable_cache for DB queries Cache expensive database operations.

Implement stale-while-revalidate Serve fast stale content while refreshing.

Consider CDN caching Cache at edge for global performance.

Database

Use connection pooling Don't create new connections per request.

Avoid N+1 queries Use JOINs or batch queries instead of loops.

Add appropriate indexes Query performance depends on proper indexing.

Cache frequent queries Don't hit the database for the same data repeatedly.

Response

Enable compression Gzip or Brotli reduces transfer size significantly.

Stream large responses Don't wait to collect all data before responding.

Use pagination Return manageable chunks instead of everything.

Return only needed fields Don't send entire records when clients need three fields.

Architecture

Use Edge Runtime when possible Lower latency for simple operations.

Parallelize independent operations Use Promise.all for concurrent fetches.

Validate early Fail fast on bad input before doing expensive work.

Handle errors gracefully Return appropriate status codes and messages.

Performance Targets

MetricTargetAction if Exceeded
Response time< 200msAdd caching, optimize queries
TTFB< 100msUse Edge Runtime
Payload size< 50KBPaginate, compress
Error rate< 1%Add retry logic, better error handling

On this page