Core concepts

API Middleware

This page will walk you through the API middleware with examples.


Core concepts

If there are shared logic that you want to execute before the handler function, you can use use or inject method to add middleware to the router. Middleware also supports async/await syntax and error handling from previous Restful API example.

// server/middleware/auth.js
import { NotFoundException, UnauthorizedException } from 'next-api-handler';

import { getUserInfo, verify } from '@/services/auth';

export const authMiddleware = async (req, res) => {
  const apiKey = req.cookies['Authorization'];

  // some async service to check apiKey
  const isValid = await verify(apiKey);

  if (!isValid) throw new UnauthorizedException();

  // another async service to fetch user info
  const user = await getUserInfo(apiKey);

  if (!user) throw new NotFoundException();

  // this has to be an object or undefined (void function)
  return { user };
};

Then you can use the middleware in the router and pass on user from req.middleware to the handler function.

// pages/api/users/me.js
import { ForbiddenException, RouterBuilder } from 'next-api-handler';

import { authMiddleware } from '@/server/middleware/auth';
import { updateUser } from '@/services/user';

const router = new RouterBuilder();

router
  .use(authMiddleware)
  .get((req) => req.middleware.user)
  .put((req) => {
    if (!req.middleware.user.isAdmin) throw new ForbiddenException();

    return updateUser(req.body);
  });

export default router.build();

Reusing the word middleware

The term middleware could be misleading here.

next-api-handler middleware is different from Express.js middleware and Next 12.0 middleware, and it is not a replacement for them nor a superset of them.

next-api-handler middleware will only be executed before the handler function, more like a shared logic as api handler middleware and can only be used in next.js API routes under pages/api directory.


Multiple middleware

Sequential execution

You can also use multiple middleware and pass on data from one middleware to another. As middleware is executed in the order of use, the data passed on will be in the same order.

// server/middleware/auth.js
import { NotFoundException, UnauthorizedException } from 'next-api-handler';

import { getEmailFromCookie, getUserByEmail } from '@/services/user';

export const cookieMiddleware = async (req, res) => {
  const email = getEmailFromCookie(req.cookies['Authorization']);

  if (!email) throw new UnauthorizedException();

  return { email };
};

export const userMiddleware = async (req, res) => {
  const user = await getUserByEmail(req.middleware.email);

  if (!user) throw new NotFoundException();

  return { user };
};

Then you can use the middleware in the router and pass on user from req.middleware to the handler function.

// pages/api/users.js
import { ForbiddenException, RouterBuilder } from 'next-api-handler';

import { cookieMiddleware, userMiddleware } from '@/server/middleware/auth';
import { updateUser } from '@/services/user';

const router = new RouterBuilder();

router
  .use(cookieMiddleware)
  .use(userMiddleware)
  .get((req) => req.middleware.user)
  .put((req) => {
    if (!req.middleware.user.isAdmin) throw new ForbiddenException();

    return updateUser(req.body);
  });

export default router.build();

Parallel execution

You can also use multiple middleware and execute them in parallel with inject method. This is useful when you want to fetch data from multiple services at the same time while still keeping the order of the data with use method in the end.

// server/middleware/auth.js
import {
  BadRequestException,
  NotFoundException,
  UnauthorizedException,
} from 'next-api-handler';

import {
  getEmailFromCookie,
  getUserByEmail,
  verifyVersion,
} from '@/services/user';

export const versionMiddleware = (req, res) => {
  const version = req.headers['x-version'];

  if (!verifyVersion(version)) throw new BadRequestException();

  return { version };
};

export const cookieMiddleware = async (req, res) => {
  const email = getEmailFromCookie(req.cookies['Authorization']);

  if (!email) throw new UnauthorizedException();

  return { email };
};

export const userMiddleware = async (req, res) => {
  const user = await getUserByEmail(req.middleware.email);

  console.log(`Client side running version ${req.middleware.version}`);

  if (!user) throw new NotFoundException();

  return { user };
};

Then you can use the middleware in the router and pass on user from req.middleware to the handler function.

// pages/api/users.js
import { ForbiddenException, RouterBuilder } from 'next-api-handler';

import {
  cookieMiddleware,
  userMiddleware,
  versionMiddleware,
} from '@/server/middleware/auth';
import { updateUser } from '@/services/user';

const router = new RouterBuilder();

router
  .inject(versionMiddleware)
  .inject(cookieMiddleware)
  .use(userMiddleware)
  .get((req) => req.middleware.user)
  .put((req) => {
    if (!req.middleware.user.isAdmin) throw new ForbiddenException();

    return updateUser(req.body);
  });

export default router.build();

Route specific middleware

You can also use middleware that is only specific to a route. This is useful when you want to use the same middleware for multiple routes.

// server/middleware/auth.js
import { NotFoundException, UnauthorizedException } from 'next-api-handler';

import { getUserInfo, verify } from '@/services/auth';

export const authMiddleware = async (req, res) => {
  const apiKey = req.cookies['Authorization'];

  // some async service to check apiKey
  const isValid = await verify(apiKey);

  if (!isValid) throw new UnauthorizedException();

  // another async service to fetch user info
  const user = await getUserInfo(apiKey);

  if (!user) throw new NotFoundException();

  // this has to be an object or undefined (void function)
  return { user };
};

export const adminMiddleware = async (req, res) => {
  if (!req.middleware.user.isAdmin) throw new ForbiddenException();
};

Then you can use the middleware in the router and pass on user from req.middleware to the handler function.

// pages/api/users/me.js
import { RouterBuilder } from 'next-api-handler';
import { authMiddleware, adminMiddleware } from '@/server/middleware/auth';
import { updateUser } from '@/services/user';

const router = new RouterBuilder();

router
  .use(authMiddleware) // This is equivalent to use('ALL', authMiddleware)
  .use('PUT', adminMiddleware)
  .get((req) => req.middleware.user)
  .put((req) => updateUser(req.body));

export default router.build();

Type safety

If you are using TypeScript, we can also enforce the response type by using generics. Here for demonstration purpose, we will put all middleware in one file. In production, you might consider splitting them into different business contexts.

// server/middleware/auth.ts
import {
  BadRequestException,
  ForbiddenException,
  NextApiHandlerWithMiddleware,
  NotFoundException,
  UnauthorizedException,
} from 'next-api-handler';

import { getUserByEmail, type User } from '@/services/user';
import { getEmailFromCookie, verifyVersion } from '@/services/user';

export type VersionMiddleware = {
  version: string,
};

export const versionMiddleware: NextApiHandlerWithMiddleware<
  VersionMiddleware
> = (req) => {
  const version = req.headers['x-version'];

  if (typeof version !== 'string' || !verifyVersion(version))
    throw new BadRequestException();

  return { version };
};

export type CookieMiddleware = {
  email: string,
};

export const cookieMiddleware: NextApiHandlerWithMiddleware<
  CookieMiddleware
> = async (req) => {
  const email = getEmailFromCookie(req.cookies['Authorization']);

  if (!email) throw new UnauthorizedException();

  return { email };
};

export type UserMiddleware = {
  user: User,
};

// Here we can pass required pre-executed middleware to the handler
export const userMiddleware: NextApiHandlerWithMiddleware<
  UserMiddleware,
  CookieMiddleware & VersionMiddleware
> = async (req) => {
  const user = await getUserByEmail(req.middleware.email);

  console.log(`Client side running version ${req.middleware.version}`);

  if (!user) throw new NotFoundException();

  return { user };
};

// Here we can pass void as the response type
export const adminMiddleware: NextApiHandlerWithMiddleware<
  void,
  UserMiddleware
> = async (req) => {
  if (!req.middleware.user.isAdmin) throw new ForbiddenException();
};

Then you can use the middleware in the router and pass on user from req.middleware to the handler function.

// pages/api/users.ts
import { BadRequestException, RouterBuilder } from 'next-api-handler';

import {
  adminMiddleware,
  cookieMiddleware,
  type UserMiddleware,
  userMiddleware,
  type VersionMiddleware,
  versionMiddleware,
} from '@/server/middleware/auth';
import { deleteUser, updateUser, type User } from '@/services/user';

const router = new RouterBuilder();

router
  .inject(versionMiddleware)
  .inject(cookieMiddleware)
  .use(userMiddleware)
  .use('PUT', adminMiddleware)
  .use('DELETE', adminMiddleware)
  .put<User>((req) => updateUser(req.body))
  // here we add required middleware type to the handler
  .get<User, UserMiddleware>((req) => req.middleware.user)
  // here we can combine multiple middleware types
  .delete<User, UserMiddleware & VersionMiddleware>(async (req) => {
    const user = await deleteUser(
      req.middleware.user.email,
      req.middleware.version
    );

    if (!user) throw new BadRequestException('Unmatched version');

    return user;
  });

export default router.build();

Sharing middleware

It might be tempted to create a router, register middleware and share between API routes, but it is not recommended. The reason is that the router will preserve usage of previous route specific middleware & handler, causing unexpected behavior.

Instead, create typed middleware in separate files and import them into the router when needed.

Previous
RESTful API