# How to Make a Next.js POST Request to External APIs: Server and Client Patterns

> Learn how to make a Next.js POST request to external APIs. Explore server-side API Routes and client-side patterns with SWR or React Query for efficient data handling.

- Repository: [Vercel/next.js](https://github.com/vercel/next.js)
- Tags: best-practices
- Published: 2026-02-19

---

**Use API Routes with Edge or Node runtimes to proxy sensitive POST requests server-side, and leverage SWR or React Query for client-side mutations to handle loading states and caching automatically.**

Making a next js post request to external services requires careful consideration of where the code executes—whether during server-side rendering, inside an API route, or in the browser. The Next.js framework provides multiple patterns for handling POST requests securely and efficiently, from Edge runtime handlers to client-side data mutations. This guide examines concrete implementations from the Vercel [`next.js`](https://github.com/vercel/next.js/blob/main/next.js) repository to help you choose the right approach for your use case.

## Server-Side Patterns for Next.js POST Requests

Server-side execution keeps sensitive credentials out of the browser and allows you to pre-render HTML with external data. Next.js offers two primary server environments: Node.js and Edge runtimes.

### Secure Proxying with API Routes

When your external API requires secret keys or OAuth tokens, create a proxy endpoint in `app/api/*/route.ts` (App Router) or `pages/api/*` (Pages Router). This pattern keeps authentication headers server-side while exposing only your own endpoint to the client.

The [`examples/with-redux/app/api/counter/route.ts`](https://github.com/vercel/next.js/blob/main/examples/with-redux/app/api/counter/route.ts) file demonstrates forwarding a POST request:

```typescript
export async function POST(request: Request) {
  const body = await request.json();
  
  // Forward to external API with secret auth
  const res = await fetch('https://api.external-service.com/data', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.API_SECRET_KEY}`
    },
    body: JSON.stringify(body)
  });
  
  const data = await res.json();
  return Response.json(data);
}

```

### Edge Runtime for Low-Latency POST Requests

For geographically distributed users or webhook handlers, use the Edge runtime by exporting `const runtime = 'edge'`. Edge functions run closer to the user, start instantly, and use the native Web `fetch` API.

The [`examples/with-stripe-typescript/app/api/webhooks/route.ts`](https://github.com/vercel/next.js/blob/main/examples/with-stripe-typescript/app/api/webhooks/route.ts) file shows an Edge handler for external webhook POST requests:

```typescript
export const runtime = 'edge';

export async function POST(req: Request) {
  const payload = await req.text();
  const signature = req.headers.get('stripe-signature');
  
  // Verify and forward to Stripe or process externally
  const response = await fetch('https://api.stripe.com/v1/events', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`,
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: payload
  });
  
  return new Response(JSON.stringify({ received: true }), {
    status: 200,
    headers: { 'Content-Type': 'application/json' }
  });
}

```

### Server-Side Rendering with getServerSideProps

For Pages Router applications requiring SEO-critical data at request time, use `getServerSideProps` to make POST requests during server rendering. The [`examples/blog/pages/posts/pages.md`](https://github.com/vercel/next.js/blob/main/examples/blog/pages/posts/pages.md) demonstrates fetching data before sending HTML to the client.

```typescript
// pages/dashboard.tsx
import { GetServerSideProps } from 'next';

export const getServerSideProps: GetServerSideProps = async (context) => {
  // Server-side POST request during SSR
  const res = await fetch('https://api.analytics.com/reports', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${process.env.ANALYTICS_KEY}` },
    body: JSON.stringify({ userId: context.req.cookies.userId })
  });
  
  const data = await res.json();
  
  return {
    props: { analyticsData: data }
  };
};

export default function Dashboard({ analyticsData }) {
  return <div>{/* Render data */}</div>;
}

```

## Client-Side Patterns for Next.js POST Requests

When user interactions trigger data changes, handle POST requests in the browser to maintain responsive UI states.

### Direct Fetch in Event Handlers

For simple form submissions, call `fetch` directly inside event handlers. This pattern requires no additional libraries but manually manages loading and error states.

```typescript
'use client';

import { useState } from 'react';

export default function ContactForm() {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsLoading(true);
    setError(null);
    
    const formData = new FormData(e.currentTarget);
    
    try {
      const res = await fetch('/api/contact', {
        method: 'POST',
        body: JSON.stringify(Object.fromEntries(formData)),
        headers: { 'Content-Type': 'application/json' }
      });
      
      if (!res.ok) throw new Error('Failed to submit');
      
      // Handle success
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error');
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      <button disabled={isLoading}>Submit</button>
      {error && <p>{error}</p>}
    </form>
  );
}

```

### Using SWR for Mutations

For automatic caching, deduplication, and revalidation, use `useSWRMutation` from the SWR library. This pattern works best when paired with an API route proxy.

```typescript
'use client';

import useSWRMutation from 'swr/mutation';

const fetcher = async (url: string, { arg }: { arg: { title: string } }) => {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(arg)
  });
  
  if (!res.ok) throw new Error('Failed to create');
  return res.json();
};

export default function NewTodo() {
  const { trigger, isMutating, error } = useSWRMutation('/api/todos', fetcher);

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    await trigger({ title: formData.get('title') as string });
    e.currentTarget.reset();
  };

  return (
    <form onSubmit={onSubmit}>
      <input name="title" required />
      <button disabled={isMutating}>Add Todo</button>
      {error && <p>Failed: {error.message}</p>}
    </form>
  );
}

```

### React Query useMutation Hook

For complex state management including optimistic updates, use TanStack Query's `useMutation`. This provides built-in error handling and rollback capabilities.

```typescript
'use client';

import { useMutation, useQueryClient } from '@tanstack/react-query';

export default function CreatePost() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: async (newPost: { title: string; body: string }) => {
      const res = await fetch('/api/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newPost)
      });
      if (!res.ok) throw new Error('Network response was not ok');
      return res.json();
    },
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
    onError: (error) => {
      console.error('Failed to create post:', error);
    }
  });

  return (
    <button 
      onClick={() => mutation.mutate({ title: 'New Post', body: 'Content' })}
      disabled={mutation.isPending}
    >
      {mutation.isPending ? 'Creating...' : 'Create Post'}
    </button>
  );
}

```

## Security and Performance Best Practices

When implementing a next js post request, protect sensitive data and optimize for your runtime environment.

### Protecting Secrets and Handling CORS

Never expose API keys in client-side code. Instead, proxy requests through API routes where `process.env` variables remain server-side. This pattern also eliminates CORS issues since the browser communicates only with your same-origin endpoint.

In [`examples/with-stripe-typescript/app/api/webhooks/route.ts`](https://github.com/vercel/next.js/blob/main/examples/with-stripe-typescript/app/api/webhooks/route.ts), the implementation demonstrates verifying signatures and injecting the `STRIPE_SECRET_KEY` environment variable without ever exposing it to the client.

### Streaming and Large Payloads

For file uploads or large JSON payloads, stream data directly to the external API rather than buffering entire requests in memory. In Edge runtimes, use `req.body.getReader()` to process streams efficiently.

### Timeouts and Error Handling

Wrap external fetch calls with `AbortController` to prevent hanging requests from blocking your API routes:

```typescript
export async function POST(req: Request) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000);
  
  try {
    const res = await fetch('https://api.external.com/data', {
      method: 'POST',
      body: await req.text(),
      signal: controller.signal
    });
    clearTimeout(timeout);
    return Response.json(await res.json());
  } catch (error) {
    clearTimeout(timeout);
    return Response.json({ error: 'Request timeout' }, { status: 504 });
  }
}

```

## Summary

- **Use API routes** in `app/api/*/route.ts` or `pages/api/*` to proxy next js post request calls, keeping secrets like `STRIPE_SECRET_KEY` server-side as shown in [`examples/with-stripe-typescript/app/api/webhooks/route.ts`](https://github.com/vercel/next.js/blob/main/examples/with-stripe-typescript/app/api/webhooks/route.ts).
- **Choose Edge runtime** (`export const runtime = 'edge'`) for low-latency webhooks or geo-distributed APIs, leveraging native `fetch` without Node.js overhead.
- **Implement client-side mutations** with `useSWRMutation` or TanStack Query to handle loading states, optimistic updates, and automatic error handling when calling your proxied API endpoints.
- **Never expose API keys** in React components; always route external requests through your own API layer to prevent CORS issues and credential leakage.
- **Stream large payloads** and implement `AbortController` timeouts in API routes to prevent memory exhaustion and hanging connections.

## Frequently Asked Questions

### How do I keep API keys secret when making a Next.js POST request?

Create an API route in [`app/api/your-route/route.ts`](https://github.com/vercel/next.js/blob/main/app/api/your-route/route.ts) that forwards the request to the external service. Store the API key in environment variables like `process.env.API_SECRET`, which are only accessible server-side. The client sends data to your API route, which then adds the authentication headers before calling the external endpoint, as demonstrated in [`examples/with-redux/app/api/counter/route.ts`](https://github.com/vercel/next.js/blob/main/examples/with-redux/app/api/counter/route.ts).

### Should I use Edge runtime or Node.js runtime for API routes?

Use **Edge runtime** (`export const runtime = 'edge'`) for low-latency requirements, webhook handlers, or when you need to execute close to the user's geographic location. Edge functions start instantly and use the Web standard `fetch` API. Use **Node.js runtime** (the default) when you need native Node modules, file system access, or complex data processing that requires the full Node.js ecosystem.

### How do I handle CORS errors when calling external APIs from the browser?

Instead of calling the external API directly from your React components—which triggers CORS preflight checks—route the request through your Next.js API route. The browser communicates with your same-origin endpoint (`/api/external`), and your server-side code makes the actual cross-origin request. This eliminates CORS restrictions while keeping your API keys secure.

### What is the best way to handle loading states during client-side POST requests?

Use data-fetching libraries like **SWR** (`useSWRMutation`) or **TanStack Query** (`useMutation`). These hooks automatically manage `isLoading`/`isMutating` states, handle error retries, and support optimistic updates that immediately reflect changes in the UI before the server confirms. This prevents you from manually tracking `useState` for loading indicators and ensures consistent error handling across your application.