next-api-layerNext API Layer
DocumentationAPI ReferenceExamples
next-api-layerNext API Layer

Production-grade API layer for Next.js with external JWT backends.

Documentation

  • Introduction
  • Installation
  • Quick Start
  • API Reference

Resources

  • Examples
  • Proxy
  • API Client
  • AuthProvider

Community

  • GitHub
  • Issues
  • Discussions
  • Contact

© 2026 Next API Layer. All rights reserved.

Created by
v0.2.3•Open Source

The Missing Layer for
External Backends

Stop wrestling with tokens, cookies, and refresh logic. A complete API layer that handles the complexity between Next.js and your JWT backend.

Get StartedView on GitHub
api.ts
// // What you write:
return api.get('client/projects/list');
// // What happens behind:
// → Token validation & refresh
// → XSS sanitization
// → CSRF protection
// → Rate limiting
// → i18n headers
The Problem

Auth shouldn't be this hard

Every Next.js + external backend project faces the same challenges

Token Juggling

Managing access tokens, refresh tokens, and their lifecycle across server and client components

Security Blind Spots

Implementing CSRF protection, XSS sanitization, and secure cookie handling correctly

Silent Refresh Logic

Handling token expiration gracefully without disrupting user experience

Repetitive Boilerplate

Writing the same auth logic in every project, every time

The Solution

One layer handles it all

A drop-in solution that abstracts away authentication complexity while giving you full control

Everything handled, automatically

Focus on building features. We handle auth infrastructure.

Secure by Default

CSRF protection, XSS sanitization, and secure cookie handling built-in.

Auto Token Refresh

Silent token refresh, automatic retry, and guest token fallback.

Zero Config Start

Works out of the box. Configure only what you need.

TypeScript First

Full type safety with comprehensive type definitions.

See the difference

Less code, more features, zero auth headaches

Without next-api-layer
300+ lines
// proxy.ts - API Proxy Route
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';

const API_URL = process.env.BACKEND_API_URL;
const TOKEN_COOKIE = 'auth_token';
const REFRESH_COOKIE = 'refresh_token';
const GUEST_COOKIE = 'guest_token';

// Token validation cache to reduce backend calls
const tokenCache = new Map<string, { valid: boolean; exp: number }>();
const pendingValidations = new Map<string, Promise<boolean>>();

export async function proxy(request: NextRequest) {
  const cookieStore = cookies();
  let token = cookieStore.get(TOKEN_COOKIE)?.value;
  
  // Check cache first
  if (token && tokenCache.has(token)) {
    const cached = tokenCache.get(token)!;
    if (cached.exp > Date.now()) {
      if (!cached.valid) {
        token = await tryRefreshToken(cookieStore);
      }
    }
  }
  
  // Validate token with backend
  if (token) {
    const isValid = await validateTokenWithBackend(token);
    if (!isValid) {
      token = await tryRefreshToken(cookieStore);
    }
  }
  
  // Fallback to guest token
  if (!token) {
    token = await getOrCreateGuestToken(cookieStore);
  }
  
  // Forward request to backend
  const backendUrl = new URL(request.url);
  backendUrl.host = new URL(API_URL).host;
  
  const headers = new Headers(request.headers);
  headers.set('Authorization', `Bearer ${token}`);
  headers.set('Accept-Language', getLocaleFromRequest(request));
  
  const response = await fetch(backendUrl, {
    method: request.method,
    headers,
    body: request.body,
  });
  
  // Handle 401 - try refresh once
  if (response.status === 401) {
    const newToken = await tryRefreshToken(cookieStore);
    if (newToken) {
      headers.set('Authorization', `Bearer ${newToken}`);
      return fetch(backendUrl, {
        method: request.method,
        headers,
        body: request.body,
      });
    }
  }
  
  // Clone response and sanitize
  const data = await response.json();
  const sanitized = sanitizeResponse(data);
  
  return NextResponse.json(sanitized, {
    status: response.status,
    headers: response.headers,
  });
}

async function validateTokenWithBackend(token: string): Promise<boolean> {
  // Deduplicate concurrent validation requests
  if (pendingValidations.has(token)) {
    return pendingValidations.get(token)!;
  }
  
  const promise = fetch(API_URL + '/auth/me', {
    headers: { 'Authorization': `Bearer ${token}` },
  }).then(res => res.ok);
  
  pendingValidations.set(token, promise);
  const result = await promise;
  pendingValidations.delete(token);
  
  // Cache result
  tokenCache.set(token, { valid: result, exp: Date.now() + 2000 });
  
  return result;
}

async function tryRefreshToken(cookieStore: any): Promise<string | null> {
  const refreshToken = cookieStore.get(REFRESH_COOKIE)?.value;
  if (!refreshToken) return null;
  
  try {
    const res = await fetch(API_URL + '/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refresh_token: refreshToken }),
    });
    
    if (!res.ok) throw new Error('Refresh failed');
    
    const { access_token, refresh_token } = await res.json();
    
    cookieStore.set(TOKEN_COOKIE, access_token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
    });
    cookieStore.set(REFRESH_COOKIE, refresh_token, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
    });
    
    return access_token;
  } catch {
    // Clear invalid tokens
    cookieStore.delete(TOKEN_COOKIE);
    cookieStore.delete(REFRESH_COOKIE);
    return null;
  }
}

async function getOrCreateGuestToken(cookieStore: any): Promise<string> {
  const existing = cookieStore.get(GUEST_COOKIE)?.value;
  if (existing) {
    const isValid = await validateTokenWithBackend(existing);
    if (isValid) return existing;
  }
  
  const res = await fetch(API_URL + '/auth/guest', { method: 'POST' });
  const { token } = await res.json();
  
  cookieStore.set(GUEST_COOKIE, token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
  });
  
  return token;
}

// sanitize.ts - XSS Protection
import sanitizeHtml from 'sanitize-html';

function sanitizeResponse<T>(data: T): T {
  if (typeof data === 'string') {
    return sanitizeHtml(data) as T;
  }
  if (Array.isArray(data)) {
    return data.map(sanitizeResponse) as T;
  }
  if (typeof data === 'object' && data !== null) {
    return Object.fromEntries(
      Object.entries(data).map(([k, v]) => [k, sanitizeResponse(v)])
    ) as T;
  }
  return data;
}

// And you STILL need:
// - Rate limit handling
// - Request timeout
// - Error boundaries
// - Loading states
// - i18n header injection
// - PUT/PATCH method spoofing
// - Response caching
// - Retry with backoff...
...and more
With next-api-layer
2 lines
import { api } from '@/lib/api';

const projects = await api.get('client/projects/list');
Token validation
Auto refresh
XSS sanitization
Error handling
Type safety
Rate limiting

Simple integration, powerful results

Get up and running in three simple steps

01

Install & Configure

Add the package and set your backend URL. That's the only required config.

02

Create Your Proxy

Set up the API route that handles all authentication logic automatically.

03

Make Secure Calls

Use the API client anywhere. Tokens, refresh, and security are handled.

Developer Experience

Built for modern Next.js

Designed to work seamlessly with App Router, Server Components, and the latest React patterns

Server Components Ready

First-class support for RSC. Fetch authenticated data directly on the server.

const user = await getServerUser();

End-to-End Type Safety

Full TypeScript support with generics for your API responses.

const data = await api.get<Project[]>('projects');

React Hooks Integration

SWR-powered hooks for client-side data fetching with automatic revalidation.

const { user, login, logout } = useAuth();

Edge Runtime Compatible

Lightweight design that works on Vercel Edge Functions and Cloudflare Workers.

export const runtime = 'edge';
Framework Agnostic

Works with any JWT backend

If it speaks JWT, we handle it. Zero vendor lock-in.

Laravel
Django
.NET
Go
Express
FastAPI
Spring Boot
Rails
Open Source

Transparent & Community-Driven

MIT licensed. No vendor lock-in. Fork it, modify it, contribute to it.

MIT License
Fully auditable code
Community contributions welcome
Issue-based support
next-api-layer
dijinar/next-api-layer
LicenseMIT
TypeScript100%
Bundle Size~8kb gzipped
View on GitHub
Open Source & Free

Ready to get started?

Install in seconds and focus on building features, not auth infrastructure.

$npm install next-api-layer
yarn add next-api-layer|pnpm add next-api-layer
Get StartedView on GitHub