@e12e/nextjs-toolkit
v1.0.2
Published
Next.js toolkit for form handling and API routes
Maintainers
Readme
@e12e/nextjs-toolkit
Next.js toolkit for form handling and API routes — less boilerplate, clean code.
Installation
npm install @e12e/nextjs-toolkit
# or
pnpm add @e12e/nextjs-toolkitPeer dependencies (must be installed separately):
npm install class-transformer class-validator react-hook-formRelated Packages
For common TypeScript application features (pagination, i18n, DTO transformation), check out @e12e/ts-omni — included as a dependency.
Table of Contents
Usage
Package provides 2 modules:
@e12e/nextjs-toolkit/api— Route builder for Next.js App Router@e12e/nextjs-toolkit/form— Resolver for react-hook-form
1. API Route Builder
Build route handlers with a fluent builder API, guard chaining, and automatic validation.
// app/api/users/route.ts
import { NextRouteBuilder } from "@e12e/nextjs-toolkit/api";
import { ApiResponse } from "@e12e/ts-omni/response";
import { IsEmail, MinLength } from "class-validator";
class CreateUserDto {
@IsEmail()
email!: string;
@MinLength(6)
password!: string;
}
export const POST = new NextRouteBuilder()
.validate(CreateUserDto)
.handle((req, { data }) => {
// data is already validated and type-safe
return ApiResponse.json({
success: true,
data: { message: `Created user ${data.email}` },
});
});Pipeline
Request flows through the pipeline in this order:
Guards → Interceptors → Validation → Handler- Guards — Auth, permissions, rate-limiting
- Interceptors — Logging, caching, request transformation
- Validation — DTO validation with class-validator
- Handler — Your business logic
Errors thrown at any stage are caught by the global error handler.
Guards
Guards run before the handler — use them for auth, rate-limiting, permissions, etc.
Each guard receives the request and the user from previous guards (if any), allowing chained authorization checks.
import { Guard } from "@e12e/nextjs-toolkit/api";
import { ApiResponse } from "@e12e/ts-omni/response";
const requireAuth: Guard = (req) => {
const token = req.cookies.get("session")?.value;
if (!token) {
return {
passed: false,
response: ApiResponse.json(
{ success: false, message: "Unauthorized" },
{ status: 401 }
),
};
}
// Return user info — passed to next guards and handler via ctx.user
return { passed: true, user: { id: "123", role: "admin" } };
};
// Second guard receives user from requireAuth
const requireAdmin: Guard = (req, user) => {
if (user?.role !== "admin") {
return {
passed: false,
response: ApiResponse.json(
{ success: false, message: "Forbidden" },
{ status: 403 }
),
};
}
return { passed: true, user };
};
export const POST = new NextRouteBuilder()
.useGuard(requireAuth) // user = { id: "123", role: "admin" }
.useGuard(requireAdmin) // receives user from requireAuth
.validate(CreateUserDto)
.handle((req, { user, data }) => {
// user.id = "123"
return ApiResponse.json({ success: true });
});HTTP Exceptions
For cleaner error handling, install @e12e/http-exception:
npm install @e12e/http-exceptionThen throw exceptions instead of manually creating responses:
import { UnauthorizedException, ForbiddenException } from "@e12e/http-exception";
const requireAuth: Guard = (req) => {
const token = req.cookies.get("session")?.value;
if (!token) throw new UnauthorizedException("No session token");
return { passed: true, user: { id: "123" } };
};
const requireAdmin: Guard = (req, user) => {
if (user?.role !== "admin") throw new ForbiddenException("Admin only");
return { passed: true, user };
};Update your error handler to handle HTTP exceptions properly:
import { HttpException } from "@e12e/http-exception";
export const globalRoute = createRouteBuilder({
onError: (error) => {
if (error instanceof HttpException) {
return ApiResponse.json(
{ success: false, message: error.message },
{ status: error.status }
);
}
return ApiResponse.json(
{ success: false, message: "Server error" },
{ status: 500 }
);
},
});Interceptors
Interceptors run after guards but before validation — use them for logging, caching, request transformation, etc.
Pipeline order: guards → interceptors → validation → handler
import { Interceptor } from "@e12e/nextjs-toolkit/api";
import { ApiResponse } from "@e12e/ts-omni/response";
// Transform request body before validation
const lowercaseEmail: Interceptor = async (req, handler) => {
if (req.body?.email) {
req.body.email = req.body.email.toLowerCase();
}
return handler();
};
// Audit logging interceptor
const auditLog: Interceptor = async (req, handler) => {
const start = Date.now();
const response = await handler();
console.log(`${req.method} ${req.url} - ${Date.now() - start}ms`);
return response;
};
export const POST = new NextRouteBuilder()
.useInterceptor(lowercaseEmail) // runs before validation
.useInterceptor(auditLog)
.validate(CreateUserDto)
.handle((req, { data }) => {
return ApiResponse.json({ success: true, data });
});Use createRouteBuilder to share interceptors across routes:
// lib/route-builder.ts
export const globalRoute = createRouteBuilder({
onError: (error) => { /* ... */ },
interceptors: [auditLog, addTimestamp],
});
// app/api/users/route.ts
export const POST = globalRoute()
.validate(CreateUserDto)
.handle((req, { data }) => {
return ApiResponse.json({ success: true, data });
});Validation
- POST / PUT / PATCH: Parses
req.json()and validates - GET / DELETE: Parses
req.nextUrl.searchParamsand validates
On validation failure, returns 400 with field-level errors:
{
"success": false,
"message": "Validation failed",
"error": {
"type": "validation_error",
"fields": {
"email": { "message": "email must be an email" }
}
}
}Error handling
Use createRouteBuilder to define a global error handler shared across routes:
// lib/route-builder.ts
import { createRouteBuilder } from "@e12e/nextjs-toolkit/api";
import { ApiResponse } from "@e12e/ts-omni/response";
export const globalRoute = createRouteBuilder({
onError: (error) => {
console.error(error);
return ApiResponse.json(
{ success: false, message: "Server error" },
{ status: 500 }
);
},
});// app/api/users/route.ts
import { globalRoute } from "@/lib/route-builder";
export const POST = globalRoute()
.validate(CreateUserDto)
.handle((req, { data }) => {
throw new Error("Something went wrong"); // Caught and forwarded to onError
});For protected routes, create a factory function that adds guards:
// lib/route-builder.ts
import { requireAuth } from "@/guards/auth.guard";
// Factory function — call it each time to get a fresh instance
export const authorizedRoute = () =>
globalRoute().useGuard(requireAuth);// app/api/users/route.ts
import { authorizedRoute } from "@/lib/route-builder";
export const GET = authorizedRoute()
.handle((req, { user }) => {
return ApiResponse.json({ success: true, data: { id: user.id } });
});2. Form Resolver
Resolver for react-hook-form powered by class-validator + class-transformer.
import { useForm } from "react-hook-form";
import { classValidatorResolver } from "@e12e/nextjs-toolkit/form";
import { IsEmail, MinLength } from "class-validator";
class LoginDto {
@IsEmail()
email!: string;
@MinLength(6)
password!: string;
}
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm<LoginDto>({
resolver: classValidatorResolver(LoginDto),
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register("email")} />
{errors.email && <span>{errors.email.message}</span>}
<input {...register("password")} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Submit</button>
</form>
);
}Error types
Each field error includes types — a map of all constraint codes to messages (auto-converted to snake_case):
// errors.email.types?.is_email → "email must be an email"
// errors.email.types?.is_not_empty → "email should not be empty"
// errors.email.type = "is_email"No need to set criteriaMode: "all" — the resolver always returns all constraints.
API Reference
NextRouteBuilder
| Method | Description |
|--------|-------------|
| useGuard(guard) | Add a guard function |
| useInterceptor(interceptor) | Add an interceptor that wraps the handler |
| validate(dtoClass) | Set DTO class for request body validation |
| handle(handler) | Finalize and return a Next.js route handler |
createRouteBuilder(options?)
| Option | Type | Description |
|--------|------|-------------|
| onError | (error, req) => ApiResponse | Global error handler for all routes |
| guards | Guard[] | Default guards applied to all routes |
| interceptors | Interceptor[] | Default interceptors applied to all routes |
Returns a factory function that creates fresh NextRouteBuilder instances on each call.
const globalRoute = createRouteBuilder({
onError: (error) => { /* ... */ },
interceptors: [auditLog],
});
// Each call returns a new instance — no state leak between routes
const route1 = globalRoute();
const route2 = globalRoute(); // independent from route1classValidatorResolver<T>(targetClass)
Returns a resolver function for useForm from react-hook-form.
License
MIT
