A unified, secure data-access layer for Next.js App Router that eliminates the need to write individual Server Actions for every API request
npm install auto-server-clientA unified, secure data-access layer for Next.js App Router that eliminates the need to write individual Server Actions for every API request.
- 🔒 Secure by default - Tokens and secrets never leave the server
- 🎯 Single API client - One configuration, use everywhere
- 🪝 React hooks - useAutoQuery and useAutoMutation for client components
- 📄 SSR support - serverQuery helper for Server Components
- 🚀 Zero boilerplate - No need to write Server Actions for each endpoint
``bash`
npm install auto-server-client
Create a proxy route handler in your Next.js app at app/api/proxy/route.ts:
`typescript
import { createServerClient } from "auto-server-client";
import { cookies } from "next/headers";
import { NextRequest, NextResponse } from "next/server";
const ALLOWED_METHODS = ["GET", "POST", "PUT", "DELETE"];
export async function POST(req: NextRequest) {
let payload;
try {
payload = await req.json();
} catch {
return NextResponse.json(
{ error: "Invalid JSON body" },
{ status: 400 }
);
}
const { method, path, body } = payload ?? {};
if (!ALLOWED_METHODS.includes(method)) {
return NextResponse.json(
{ error: "Invalid method" },
{ status: 400 }
);
}
if (typeof path !== "string" || !path.startsWith("/")) {
return NextResponse.json(
{ error: "Invalid path" },
{ status: 400 }
);
}
const serverClient = createServerClient({
baseURL: process.env.API_URL!,
getToken: async () => (await cookies()).get("accessToken")?.value,
});
try {
let data;
switch (method) {
case "GET":
data = await serverClient.get(path);
break;
case "POST":
data = await serverClient.post(path, body);
break;
case "PUT":
data = await serverClient.put(path, body);
break;
case "DELETE":
data = await serverClient.delete(path);
break;
}
return NextResponse.json(data);
} catch (error) {
return NextResponse.json(
{ error: "Request failed" },
{ status: 500 }
);
}
}
`
Add your API base URL to .env.local:
`env`
API_URL=https://api.example.com
#### Client Components (with hooks)
`typescript
"use client";
import { useAutoQuery, useAutoMutation } from "auto-server-client";
export default function TodoList() {
// Fetch data
const { data, loading, error } = useAutoQuery("/todos");
// Mutate data
const { mutate, loading: isCreating } = useAutoMutation("/todos", "POST");
const handleCreate = async () => {
try {
const newTodo = await mutate({ title: "New Todo", completed: false });
console.log("Created:", newTodo);
} catch (error) {
console.error("Failed to create:", error);
}
};
if (loading) return
return (
#### Server Components
The package exports a
serverQuery helper that works out of the box if your token is stored in a cookie named accessToken:`typescript
import { serverQuery } from "auto-server-client";export default async function TodoPage() {
// This runs on the server and can access cookies/headers securely
const todo = await serverQuery("/todos/1");
return (
{todo.title}
Status: {todo.completed ? "Done" : "Pending"}
);
}
`> Note: The default
serverQuery expects:
> - process.env.API_URL to be set
> - An accessToken cookie for authentication
>
> If you need different token handling or cookie names, create a custom helper (see Custom Server Query section).API Reference
$3
Creates a server-side API client with automatic token injection.
Parameters:
-
config.baseURL (string, required): Base URL for your API
- config.getToken (function, optional): Async function that returns the authentication tokenReturns:
ServerClient object with methods: get, post, put, deleteExample:
`typescript
const client = createServerClient({
baseURL: "https://api.example.com",
getToken: async () => {
const cookies = await import("next/headers").then(m => m.cookies());
return cookies.get("accessToken")?.value;
},
});const data = await client.get("/users");
`$3
React hook for fetching data in client components.
Parameters:
-
path (string): API endpoint path (e.g., "/users/123")Returns:
`typescript
{
data?: T;
loading: boolean;
error?: string;
}
`Example:
`typescript
const { data, loading, error } = useAutoQuery("/users/1");
`$3
React hook for mutations in client components.
Parameters:
-
path (string): API endpoint path
- method ("POST" | "PUT" | "DELETE"): HTTP methodReturns:
`typescript
{
mutate: (body?: unknown) => Promise;
data?: T;
loading: boolean;
error?: string;
}
`Example:
`typescript
const { mutate, loading } = useAutoMutation("/users", "POST");
const newUser = await mutate({ name: "John", email: "john@example.com" });
`$3
Helper function for fetching data in Server Components.
Parameters:
-
path (string): API endpoint pathReturns:
PromiseRequirements:
-
process.env.API_URL must be set
- An accessToken cookie must be present (for authenticated requests)Example:
`typescript
const user = await serverQuery("/users/1");
`Advanced Usage
$3
If the default
serverQuery doesn't match your setup (different cookie name, token source, etc.), create your own helper:`typescript
// lib/server-query.ts
import { cookies } from "next/headers";
import { createServerClient } from "auto-server-client";export async function serverQuery(path: string): Promise {
const client = createServerClient({
baseURL: process.env.API_URL!,
getToken: async () => (await cookies()).get("accessToken")?.value,
});
return client.get(path);
}
`Then use it in Server Components:
`typescript
import { serverQuery } from "@/lib/server-query";export default async function Page() {
const data = await serverQuery("/users");
return
{/ render data /};
}
`$3
You can customize how tokens are retrieved based on your authentication setup:
`typescript
import { headers } from "next/headers";const client = createServerClient({
baseURL: process.env.API_URL!,
getToken: async () => {
const headersList = await headers();
return headersList.get("authorization")?.replace("Bearer ", "");
},
});
`$3
The hooks return error states you can handle:
`typescript
const { data, error, loading } = useAutoQuery("/todos");if (error) {
// Handle error - show toast, redirect, etc.
return ;
}
`For mutations, wrap in try/catch:
`typescript
const { mutate } = useAutoMutation("/todos", "POST");try {
await mutate({ title: "New Todo" });
} catch (error) {
// Handle error
console.error("Mutation failed:", error);
}
`Security Considerations
- ✅ Tokens are server-only: The
getToken function runs on the server, so tokens never appear in client-side code
- ✅ Proxy validation: The proxy route validates methods and paths before forwarding requests
- ✅ Cookie-based auth: Tokens should be stored in HTTP-only cookies for maximum security
- ⚠️ Path validation: Ensure your proxy validates paths to prevent SSRF attacks
- ⚠️ Rate limiting: Consider adding rate limiting to the proxy route in productionHow It Works
1. Client Components: Components call hooks (
useAutoQuery/useAutoMutation) which send requests to /api/proxy
2. Proxy Route: The proxy route receives the request, extracts the token from cookies, and forwards the request to your API
3. Server Components: Use serverQuery or createServerClient directly to fetch data during SSR
4. Token Injection: Tokens are automatically injected as Bearer tokens in the Authorization headerWhy Use This?
Traditional Next.js App Router patterns require:
- Writing a Server Action for every API call
- Duplicating fetch logic across files
- Managing tokens in multiple places
- Client components unable to call authenticated APIs directly
auto-server-client` solves this by:All without exposing tokens or secrets to the browser.
MIT