next-safe-guard
v1.0.2
Published
Turnkey type-safe, rate-limited, and audited Next.js Server Action wrapper with built-in LRU cache, React hooks, and Redis support.
Maintainers
Readme
🛡️ next-safe-guard
Turnkey type-safe, rate-limited, and audited Next.js Server Action wrapper.
Stop writing boilerplate validation, rate limiting, and error handling for every server action. next-safe-guard wraps your logic in a single composable function with Zod validation, built-in LRU rate limiting (no Redis required), audit logging, and a React hook for seamless client-side consumption.
🎬 Demo
✨ Features
| Feature | Description |
|---|---|
| Zod Validation | Automatically validates input and returns structured field-level errors |
| Rate Limiting | Built-in in-memory LRU sliding-window limiter (works on Vercel serverless) |
| Redis Support | Plug in your own Redis/Upstash store for distributed rate limiting |
| Audit Logging | Hook into every action execution (success, failure, rate-limit hit) |
| Context Injection | Run auth/session checks before the action executes |
| React Hook | useServerAction() with loading state, error handling, and race condition prevention |
| Type-Safe | Full TypeScript inference from Zod schema to handler to client |
🚀 Installation
npm install next-safe-guard zod💡 Quick Start
1. Create a Safe Server Action
// app/actions/updateUser.ts
'use server';
import { safeAction } from 'next-safe-guard';
import { z } from 'zod';
const UpdateUserSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email address'),
});
export const updateUser = safeAction(
UpdateUserSchema,
async (input, ctx) => {
// Your business logic here
// `input` is fully typed as { name: string; email: string }
// `ctx` contains the resolved context (e.g., userId)
return { updated: true, name: input.name };
},
{
// Auth check — runs before the handler
getContext: async () => {
const session = await getSession();
if (!session) throw new Error('Unauthorized');
return { userId: session.user.id };
},
// Rate limit: 10 requests per 60 seconds per IP
rateLimit: { limit: 10, window: 60 },
// Audit trail for every execution
onAudit: (log) => {
console.log(`[AUDIT] ${log.actionName} | ${log.success ? '✅' : '❌'} | IP: ${log.ip}`);
},
}
);2. Consume in a Client Component
// app/components/UserForm.tsx
'use client';
import { useServerAction } from 'next-safe-guard';
import { updateUser } from '../actions/updateUser';
export function UserForm() {
const { execute, data, error, isLoading, reset } = useServerAction(updateUser);
const handleSubmit = async (formData: FormData) => {
const result = await execute({
name: formData.get('name') as string,
email: formData.get('email') as string,
});
if (result.success) {
alert('User updated!');
}
};
return (
<form action={handleSubmit}>
<input name="name" placeholder="Name" />
{error?.fieldErrors?.name && <p className="error">{error.fieldErrors.name[0]}</p>}
<input name="email" placeholder="Email" />
{error?.fieldErrors?.email && <p className="error">{error.fieldErrors.email[0]}</p>}
<button type="submit" disabled={isLoading}>
{isLoading ? 'Saving...' : 'Save'}
</button>
{error && !error.fieldErrors && <p className="error">{error.message}</p>}
</form>
);
}🔧 Custom Redis Rate Limiter
For production deployments across multiple serverless instances, plug in a Redis-backed store:
import { safeAction, type RateLimitStore } from 'next-safe-guard';
import { Redis } from '@upstash/redis';
const redis = new Redis({ url: process.env.REDIS_URL!, token: process.env.REDIS_TOKEN! });
const redisStore: RateLimitStore = {
async increment(key, limit, window) {
const current = await redis.incr(key);
if (current === 1) await redis.expire(key, window);
const ttl = await redis.ttl(key);
return {
success: current <= limit,
limit,
remaining: Math.max(0, limit - current),
reset: Date.now() + ttl * 1000,
};
},
};
export const myAction = safeAction(schema, handler, {
rateLimit: { limit: 20, window: 60 },
rateLimitStore: redisStore,
});📦 API Reference
safeAction(schema, handler, config?)
| Parameter | Type | Description |
|---|---|---|
| schema | z.ZodType | Zod schema for input validation |
| handler | (input, context) => Promise<T> | Your business logic |
| config.getContext | () => Promise<T> | Auth/session resolver (throws to reject) |
| config.rateLimit | { limit, window } | Requests allowed per window (seconds) |
| config.onAudit | (log) => void | Callback for every action execution |
| config.rateLimitStore | RateLimitStore | Custom store (defaults to in-memory LRU) |
useServerAction(action)
Returns: { execute, data, error, isLoading, reset }
📄 License
MIT
