Coder Social home page Coder Social logo

next-api-middleware's Introduction

Next.js API Middleware

npm npm license GitHub Workflow Status Codecov

Introduction

⚠️ This library was written to support API routes that use the Next.js Pages Router. It has not been tested with the App Router.

Next.js API routes are a ridiculously fun and simple way to add backend functionality to a React app. However, when it comes time to add middleware, there is no easy way to implement it.

The official Next.js docs recommend writing functions inside your API route handler. This is a huge step backward compared to the clean APIs provided by Express.js or Koa.js.

This library attempts to provide minimal, clean, composable middleware patterns that are both productive and pleasant to use.

Table of Contents

Quick Start

import { label, Middleware } from "next-api-middleware";
import * as Sentry from "@sentry/nextjs";
import nanoid from "nanoid";

// 1 – Create middleware functions

const captureErrors: Middleware = async (req, res, next) => {
  try {
    // Catch any errors that are thrown in remaining
    // middleware and the API route handler
    await next();
  } catch (err) {
    const eventId = Sentry.captureException(err);

    res.status(500);
    res.json({ error: err });
  }
};

const addRequestId: Middleware = async (req, res, next) => {
  // Let remaining middleware and API route execute
  await next();

  // Apply header
  res.setHeader("X-Response-ID", nanoid());
};

// 2 – Use `label` to assemble all middleware

const withMiddleware = label(
  {
    addRequestId,
    sentry: captureErrors, // <-- Optionally alias middleware
  },
  ["sentry"] // <-- Provide a list of middleware to call automatically
);

// 3 – Define your API route handler

const apiRouteHandler = async (req, res) => {
  res.status(200);
  res.send("Hello world!");
};

// 4 – Choose middleware to invoke for this API route

export default withMiddleware("addRequestId")(apiRouteHandler);

How It Works

My mental model for how this library handles middleware functions is that of a "winding and unwinding stack."

Let's imagine you've used label to add two middleware functions to an API route.

When a request comes in, this is a rough impression of how that request makes its way through all middleware functions, the API route handler itself, and then back up through the middleware.

              |-----------------|-----------------|--------------------|
              |  Middleware #1  |  Middleware #2  | API Route Handler  |
              |-----------------|-----------------|--------------------|
              |                 |                 |                    |
Request ------|----> Setup -----|----> Setup -----|-->------|          |
              |                 |                 |         |          |
              |-----------------|-----------------|         V          |
              |                 |                 |                    |
              |   await next()  |   await next()  |     API stuff      |
              |                 |                 |                    |
              |-----------------|-----------------|         |          |
              |                 |                 |         |          |
Response <----|--- Teardown <---|--- Teardown <---|---------|          |
              |                 |                 |                    |
              |-----------------|-----------------|--------------------|

While this is a crummy ASCII diagram, I think it gives the right impression. The request winds its way though each middleware function in succession, hits the API route handler, and then proceeds to "unwind" its way through the stack.

Every middleware function has the opportunity to go through three phases:

  1. Setup
  2. Waiting
  3. Teardown

The "Setup" phase covers everything that happens before await next(). The "Waiting" phase is really just await next(). The "Teardown" phase is the remaining code within a middleware function after await next().

It is worth noting that although these phases are available to all middleware functions, you don't need to take advantage of them all.

For example, in error catching middleware you might simply wrap await next() in a try / catch block. On the other hand, you might have request timing middleware that captures a start time during the setup phase, waits, and then captures a finish time in the teardown phase.

APIs

label

This is the primary utility for creating reusuable collections of middleware for use throughout many Next.js API routes.

const withMiddleware = label(middleware, defaults);

Parameters

  • middleware: an object containing middleware functions or arrays of middleware
  • defaults: (optional) an array of middleware keys that will be invoked automatically

Return Value

label returns a function (conventionally referred to as withMiddleware) that uses currying to accept a list of middleware names to be invoked, followed by a Next.js API handler function.

Typically, withMiddleware will be imported in API route files and used at the default export statement:

import { withMiddleware } from "../helpers/my-middleware";

const apiRouteHandler = async (req, res) => {
  ...
}

export default withMiddleware("foo", "bar", "baz")(apiRouteHandler);

Though label could contain many middleware functions, the actual middleware invoked by an API route is determined by the names passed in to withMiddleware.

Examples

Basic Use
const logErrors = async (req, res, next) => {
  try {
    await next();
  } catch (error) {
    console.error(error);
    res.status(500);
    res.json({ error });
  }
};

const withMiddleware = label({
  logErrors,
});

// export default withMiddleware("logErrors")(apiRouteHandler);
Aliases
const withMiddleware = label({
  error: logErrors,
});

// export default withMiddleware("error")(apiRouteHandler);
Groups
import { foo, bar, baz } from "./my-middleware";

const withMiddleware = label({
  error: logErrors,
  myGroup: [foo, bar, baz],
});

// export default withMiddleware("error", "myGroup")(apiRouteHandler);
Defaults
const withMiddleware = label(
  {
    error: logErrors,
    myGroup: [foo, bar, baz],
  },
  ["error"]
);

// export default withMiddleware("myGroup")(apiRouteHandler);

use

This utility accepts middleware functions directly and executes them all in order. It is a simpler alternative to label that can be useful for handling one-off middleware functions.

const withInlineMiddleware = use(...middleware);

Parameters

  • middleware: a list of middleware functions and/or arrays of middleware functions

Return Value

use returns a function that accepts a Next.js API route handler.

Examples

CORS
import { use } from "next-api-middleware";
import cors from "cors";

const apiRouteThatOnlyNeedsCORS = async (req, res) => {
  ...
}

export default use(cors())(apiRouteThatOnlyNeedsCORS);

Usage Guide

See EXAMPLES.md for more detailed examples of label and use.

Advanced

Middleware Factories

Since use and label accept values that evaluate to middleware functions, this provides the opportunity to create custom middleware factories.

Here's an example of a factory that generates a middleware function to only allow requests with a given HTTP method:

import { Middleware } from "next-api-middleware";

const httpMethod = (
  allowedHttpMethod: "GET" | "POST" | "PATCH"
): Middleware => {
  return async function (req, res, next) {
    if (req.method === allowedHttpMethod || req.method == "OPTIONS") {
      await next();
    } else {
      res.status(404);
      res.end();
    }
  };
};

export const postRequestsOnlyMiddleware = httpMethod("POST");

Middleware Signature

Middleware is inspired by the asyncronous middleware style popularized by Koa.js.

type Middleware<Request = NextApiRequest, Response = NextApiResponse> = (
  req: Request,
  res: Response,
  next: () => Promise<void>
) => Promise<void>;

Alternatives

next-api-middleware's People

Contributors

dependabot[bot] avatar depfu[bot] avatar htunnicliff avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

next-api-middleware's Issues

Unable to add middleware to any PAGE request

Is your feature request related to a problem? Please describe.
I installed the lib to add a threadLocal like behavior to any of my pages : storing request scoped variables to use throughout the request processing. I needed the lib to init the variables at page request and clean them after the page is processed. I cannot find any way to use the lib for any page (like a app.all() in expressjs)
Describe the solution you'd like
I would like to apply the middleware to any page rendering
Describe alternatives you've considered
I can use nextjs middleware rewrites
Additional context
Also, note it is not easy from the docs to understand you only handle API requests, I lost some time before I realized it

Promise chain is never terminated if middleware aborts request

Describe the bug

I'm using next-api-middleware to create a middleware that does some validation and aborts the API handler if the validation fails. This middleware never calls next() and instead aborts the stack early and returns an error response instead. When this happens, next-api-middleware will never resolve the main Promise chain for the API endpoint handler. This is not a problem in Next.js itself, which will happily still generate a valid HTTP response, but it's an issue when trying to unit test API endpoint handlers as the you cannot await the handlers anymore.

To Reproduce

Minimally reproducible repo here: https://stackblitz.com/edit/nextjs-uzugkb?file=README.md

You can run that repo with npm run dev and visit /api/hello to see that the middleware and API route work as expected (returns an error). However, if you run the tests npm run test you'll see that the tests timeout because the unit test code tries to await the API handler response and it never resolves.

Expected behavior

Using a middleware that next calls next() works fine. However, when you use a middleware that doesn't ever call next() and instead returns a non-undefined response, it should abort the promise chain and call finish() in the executor. Right now it doesn't.

Additional info

Additionally, I noticed that any API handlers wrapped with middleware will no longer resolve with the value of the handler. For example:

// api/hello.js
export default function handler () {
  return Promise.resolve('hello')
}

// api/hello.test.js
import handler from './hello';
import {use} from 'next-api-middleware';

// This works!
it('should resolve with the value', async () => {
    const response = await handler();
    expect(response).toEqual('hello');
});

// This doesn't work
it('should resolve with the value when wrapped with middleware', async () => {
    const middleware = (req, res, next) => {
        // A middleware that does nothing and just continues the promise chain
        return next();
    }

    const response = await use(middleware)(handler)();
    expect(response).toEqual('hello');
});

typescript error with cors example on documentation

When using the cors example code from the documentation page, it gives typescript error:

TS2345: Argument of type '(req: NextApiRequest, res: { statusCode?: number | undefined; setHeader(key: string, value: string): any; end(): any; }, next: (err?: any) => any) => void' is not assignable to parameter of type 'Middleware<NextApiRequest, NextApiResponse<any>> | Middleware<NextApiRequest, NextApiResponse<any>>[]'.   Type '(req: NextApiRequest, res: { statusCode?: number | undefined; setHeader(key: string, value: string): any; end(): any; }, next: (err?: any) => any) => void' is not assignable to type 'Middleware<NextApiRequest, NextApiResponse<any>>'.     Type 'void' is not assignable to type 'Promise<void>'.
import { use } from "next-api-middleware";
import cors from "cors";

const apiRouteThatOnlyNeedsCORS = async (req, res) => {
  ...
}

export default use(cors())(apiRouteThatOnlyNeedsCORS);

Please advice on how to resolve it. Thanks

Depfu Error: No dependency files found

Hello,

We've tried to activate or update your repository on Depfu and couldn't find any supported dependency files. If we were to guess, we would say that this is not actually a project Depfu supports and has probably been activated by error.

Monorepos

Please note that Depfu currently only searches for your dependency files in the root folder. We do support monorepos and non-root files, but don't auto-detect them. If that's the case with this repo, please send us a quick email with the folder you want Depfu to work on and we'll set it up right away!

How to deactivate the project

  • Go to the Settings page of either your own account or the organization you've used
  • Go to "Installed Integrations"
  • Click the "Configure" button on the Depfu integration
  • Remove this repo (htunnicliff/next-api-middleware) from the list of accessible repos.

Please note that using the "All Repositories" setting doesn't make a lot of sense with Depfu.

If you think that this is a mistake

Please let us know by sending an email to [email protected].


This is an automated issue by Depfu. You're getting it because someone configured Depfu to automatically update dependencies on this project.

`use` and `label` lead Next to think no response was sent

Describe the bug

Any async endpoint that I pass middleware to using use or label ultimately responds to the front-end as expected, but it leads Next to log the below string in development.

API resolved without sending a response for /api/<rest of uri>, this may result in stalled requests.

To Reproduce
Steps to reproduce the behavior:

  1. Create a function withMiddleware as shown below;
  2. Create a basic asynchronous handler.
  3. Run the application
  4. Call the endpoint
  5. Observe the logs in the terminal
const withMiddleware = label({
  development: [morgan("dev") as ExpressMiddleware],
});

export default withMiddleware(process.env.NODE_ENV)(
  async (req: NextApiRequest, res: NextApiResponse) => {
    await wait(0);
    res.status(200).send([{ value: BookmarkStatus.Saved }]);
  }
);

// helper function
function wait(ms: number = 0) {
  return new Promise(res => setTimeout(res, ms));
}

Expected behavior

I expect everything to work the way it does, except that Next should process that the handler is working.

You can see the expected behavior by removing the call to wait from the sample code.

Additional context

I haven't tried any other methods, so I am explicitly calling out the two methods that I have tried

[question] `res.setHeader` after `status(code).json(body)` fails with `ERR_HTTP_HEADERS_SENT`

Describe the bug

I have recently started using your middleware package on a personal project and I quite enjoyed following through with the docs and implementing what I needed. At some point, I saw the following example.

// request-tracing.pipe.ts`
export async function requestTracingPipe(
  _req: NextApiRequest,
  res: NextApiResponse,
  next: () => Promise<void>
): Promise<void> {
  res.setHeader("X-Timing-Start", new Date().toISOString())

  await next()

  res.setHeader("X-Response-ID", nanoid())
  res.setHeader("X-Timing-End", new Date().toISOString())
}

However, when trying to setHeader after responding to the client on the API Route, I get the following error:

Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

I have run the debugger multiple times and res is only called once. Also, if I disable request-tracing.pipe.ts the error stops occurring.

Here is a snippet of the code that runs before the middleware.

async function getHandler(req: NextApiRequest, res: NextApiResponse) {
  const { search } = req.query

  // res.status(200).json(data) executes and the next middleware to execute is `request-tracing.pipe.ts`
  if (search) {
    const data = await forwardGeocode(search as string)
    res.status(200).json(data) 👈
  } else {
    throw new AppError(400, `Query string must define "search" key value pair.`)
  }
}

async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === "GET") {
    await getHandler(req, res)
  }
}

export default withMiddleware()(METHODS_GUARD)(handler)

Question

Has this happened before? Are there any Next API changes we are not aware of?

Expected behavior

After performing res.status(code).json(body) I expected the res to still pass through request-tracing.pipe.ts before being returned to the client, so that, timing headers can be set. However, this might not be a bug related to next-api-middleware.

Desktop (please complete the following information):

  • OS: Ubuntu
  • Version: 20.04
  • Next 12.2.3

Invalid example with httpMethod?

Describe the bug
It's possible I'm misunderstanding usage, but after reading the executor code, I think the httpMethod example in the readme is buggy. It appears that executor expects a middleware to always either call next or to throw something. Without either, cleanupPromise is never resolved or rejected, and the line await cleanupPromise.promise; is forever pending.

To Reproduce
Steps to reproduce the behavior:

  1. Add the httpMethod example to your codebase.
  2. Make a request to a handler with an invalid HTTP method.
  3. res.status and res.end are called properly, but the middleware never resolves.

Expected behavior
If a middleware promise resolves and next was never called, it should be assumed that none of the downstream middlewares or handlers should be called, and the teardown stack should be resolved.

As a workaround, I've decided to add a throw new Error() after res.end() is called in the httpMethod example, but this rejects the whole handler+middleware call. This isn't what I want because 404ing an invalid HTTP verb is an example of the handler handling the request properly.

Feature: Middleware with parameters

Is your feature request related to a problem? Please describe.

I would like to create reuseable middleware for input data validation. This middleware will read the req.body value and validate it against a provided schema. I would love to use next-api-middleware for this, but unfortunately it doesn't support middleware with optional parameters.

Describe the solution you'd like

Ideally, middleware would allow me to pass input, something like this:

const validateInput: NextMiddleware = async (
    req,
    res,
    next,
    options // This would be new allow me to pass in a schema
) => {
    const postData = JSON.parse(req.body);
    
    try {
        options.schema.validate(postData);
    } catch (err) {
        res.status(422).json({
            errors: [`Your input is invalid: ${err.message}`],
	})
    }

    return next();
};

I could then use this middleware in my API handler:

import {object, string} from 'yup';

const apiRouteHandler = async (req, res) => {
  res.status(200);
  res.send("Hello world!");
};

const schema = object({
    name: string().required(),
});

export default use(validateInput(schema))(apiRouteHandler);

Describe alternatives you've considered

At the moment, I avoid using next-api-middleware for this reason, and end up writing custom code and injecting it into all my API methods.

Additional context

Furthermore (but maybe as a separate request), it would also be great if middleware could pass data down into the API handler too. For example, I want to use next-api-middleware to create a middleware for authentication. If authentication passes, I want to pass the current user session object into the API handler so the API handler can react to the user object.

Add a type safe way for extending the request type

Is your feature request related to a problem? Please describe.
In our middleware we add values onto the request object in order to pass them into the API handler. These values are however not type safe. The middleware request type expects the type to satisfy NextApiRequest. For example, if we want to setup the endpoint with a session we will need to make it optional.

export type NextApiRequestExtension = NextApiRequest & {
  session?: Session | null;
  ...
}

The code above works. Then we can use it like so:

export const addSession: Middleware<NextApiRequestExtension, NextApiResponse> = async (
  req: NextApiRequestExtension,
  res: NextApiResponse,
  next: () => Promise<void>,
) => {
  try {
    ...
    (req as NextApiRequestExtension).session = session;
    await next();
  } catch (err) {
   ...
  }
};

Then in the API handler we get the following:
Screenshot 2023-07-13 at 2 34 29 PM

In order to ensure the session is there we need to do a check inside each api handler instead of one check in the middleware.

Describe the solution you'd like

A great solution would be to allow a type safe extension of the request object.

Say we apply a middleware to a request:

// helper file

export const addSession: Middleware<NextApiRequest, NextApiResponse> = async (
  req: NextApiRequestExtension,
  res: NextApiResponse,
  next: () => Promise<void>,
) => {
  try {
    ...
    if (!session) throw new Error('Invalid session');
    (req as NextApiRequestExtension).session = session;
    await next();
  } catch (err) {
   ...
  }
};

export const withMiddleware = label(
  {
    addSession
  }
);

..............................................
// Api handler file

const handler = async (req) => {
   ...
}

export default withMiddleware('addSession')(handler)

The req on async (req) would have the req.session type listed as defined because it is added to the request object inside the addSession. We ensure it is defined inside the addSession middleware using the line if (!session) throw new Error('Invalid session');.

This would save us from checking if it is defined in every api handler. It would also allow make it typesafe depending on which api handler was attached.

If we didn't include the addSession middleware, i.e. export default withMiddleware()(handler). I'd expect the req object to not have the session on it and for there to be no typeahead.

Describe alternatives you've considered

We are currently using the undefined approach of extending the request object. It works, but requires us to add checks in each api handler.

I've tried altering the request object on the label method by doing label<LabeledMiddleware<NextApiRequestExtended, NextResponse>> but it runs into the same issue where it says NextApiRequestExtended does not satisfy NextApiRequest.

Open to feedback or alternative solutions.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.