A simple, type-safe middleware system for Next.js with route protection and built-in authentication rules.
- Type-Safe: Full TypeScript support with excellent IntelliSense
- Simple API: Intuitive fluent interface for building middleware
- Route Protection: Easy-to-use route guards with exact and prefix matching
- Built-in Rules: Common authentication and authorization patterns included
- Flexible: Create custom rules for any use case
- Lightweight: No dependencies beyond Next.js
npm install next-middleware-toolkit
# or
pnpm add next-middleware-toolkit
# or
yarn add next-middleware-toolkit
# or
bun add next-middleware-toolkit// middleware.ts
import { MiddlewareBuilder, Rules } from 'next-middleware-toolkit';
import { NextRequest } from 'next/server';
type User = {
id: string;
email: string;
role: string;
permissions: string[];
};
const fetchUser = async (req: NextRequest): Promise<User | null> => {
// Your user fetching logic here
const session = await getSession(req);
return session ? await getUserData(session.userId) : null;
};
const middleware = new MiddlewareBuilder({
fetchUser,
baseUrl: process.env.NEXT_PUBLIC_BASE_URL, // Optional: for redirects
})
.exact('/login', Rules.isNotLoggedIn())
.exact('/register', Rules.isNotLoggedIn())
.prefix('/dashboard', Rules.isLoggedIn())
.prefix('/admin', Rules.isLoggedIn(), Rules.hasRole('admin'))
.build();
export default middleware;
export const config = {
matcher: ['/((?!api/|_next/|_static|[\\w-]+\\.\\w+).*)'],
};The main class for building middleware.
interface MiddlewareBuilderOptions<T> {
// Function to fetch user data for each request
fetchUser: (req: NextRequest) => Promise<T | null>;
// Optional: Base URL for redirects (defaults to request origin)
baseUrl?: string;
}.exact(path, ...rules)
Add an exact route match. The path must match exactly for the rules to execute.
.exact('/profile', Rules.isLoggedIn())
.exact('/users/[userId]', Rules.isLoggedIn()).prefix(path, ...rules)
Add a prefix route match. Any path starting with the prefix will match.
.prefix('/dashboard', Rules.isLoggedIn())
.prefix('/api/admin', Rules.hasRole('admin')).build()
Build and return the final middleware handler.
const middleware = builder.build();
export default middleware;All built-in rules are available via the Rules object.
Requires the user to be logged in. Redirects to /sign-in if not authenticated.
.exact('/profile', Rules.isLoggedIn())Requires the user to NOT be logged in. Redirects to / if already authenticated.
.exact('/login', Rules.isNotLoggedIn())Requires the user to have a specific role. Returns 403 if the user doesn't have the required role.
.prefix('/admin', Rules.hasRole('admin'))Works with both user.role (string) and user.roles (array).
Requires the user to have a specific permission. Returns 403 if the user doesn't have the required permission.
.prefix('/api/users', Rules.hasPermission('read:users'))Requires user.permissions to be an array.
Always redirects to the specified destination.
.exact('/old-path', Rules.redirectTo('/new-path'))Create a custom rule with your own logic.
.exact('/profile/[userId]',
Rules.isLoggedIn(),
Rules.custom(({ data, params }) => {
// Only allow users to access their own profile
if (data?.id !== params.userId) {
return Responses.forbidden('Cannot access another user\'s profile');
}
return null; // Allow the request to continue
})
)Helper functions for creating responses.
Continue to the next middleware or handler.
return Responses.next();Redirect to a URL. Uses the base URL configured in MiddlewareBuilder.
return Responses.redirect('/login');Return a JSON response.
return Responses.json({ error: 'Invalid request' }, 400);Return a 401 Unauthorized response.
return Responses.unauthorized('Login required');Return a 403 Forbidden response.
return Responses.forbidden('Access denied');Return a 404 Not Found response.
return Responses.notFound('Page not found');Create custom rules for complex authorization logic:
const requireOwnership = Rules.custom(({ data, params }) => {
const userId = params.userId;
const isOwner = data?.id === userId;
const isAdmin = data?.role === 'admin';
if (!isOwner && !isAdmin) {
return Responses.forbidden('You can only modify your own resources');
}
return null; // Allow the request
});
const middleware = new MiddlewareBuilder({ fetchUser })
.exact('/users/[userId]/edit', Rules.isLoggedIn(), requireOwnership)
.build();Access dynamic route parameters in your rules:
.exact('/posts/[postId]/comments/[commentId]',
Rules.custom(({ params }) => {
console.log(params.postId); // Access post ID
console.log(params.commentId); // Access comment ID
return null;
})
)Chain multiple rules together. They execute in order and stop at the first one that returns a response:
.prefix('/admin',
Rules.isLoggedIn(),
Rules.hasRole('admin'),
Rules.custom(({ data }) => {
// Additional custom checks
if (!data?.emailVerified) {
return Responses.forbidden('Email must be verified');
}
return null;
})
)Every rule receives a context object:
interface MiddlewareContext<T> {
data: T | null; // User data from fetchUser
req: NextRequest; // Next.js request object
path: string; // Current path
params: Record<string, string>; // Route parameters
}The library is fully typed. Define your user type for complete type safety:
type User = {
id: string;
email: string;
role: 'admin' | 'user' | 'moderator';
permissions: string[];
};
const middleware = new MiddlewareBuilder<User>({
fetchUser: async (req) => {
// Return type is automatically User | null
return await getUser(req);
},
})
.exact(
'/profile',
Rules.custom(({ data }) => {
// data is typed as User | null
console.log(data?.email);
return null;
}),
)
.build();const middleware = new MiddlewareBuilder({ fetchUser })
.exact('/login', Rules.isNotLoggedIn())
.exact('/register', Rules.isNotLoggedIn())
.prefix('/app', Rules.isLoggedIn())
.build();const middleware = new MiddlewareBuilder({ fetchUser })
.prefix('/admin', Rules.isLoggedIn(), Rules.hasRole('admin'))
.prefix('/moderator', Rules.isLoggedIn(), Rules.hasRole('moderator'))
.prefix('/dashboard', Rules.isLoggedIn())
.build();const middleware = new MiddlewareBuilder({ fetchUser })
.prefix('/api/users', Rules.hasPermission('read:users'))
.prefix('/api/posts', Rules.hasPermission('read:posts'))
.exact('/api/admin/settings', Rules.hasPermission('write:settings'))
.build();const middleware = new MiddlewareBuilder({ fetchUser })
.exact(
'/profile/[userId]',
Rules.isLoggedIn(),
Rules.custom(({ data, params }) => {
if (data?.id !== params.userId && data?.role !== 'admin') {
return Responses.forbidden();
}
return null;
}),
)
.build();MIT License - see LICENSE file for details.