Next.js: [RFC] server(less) middleware

Created on 1 May 2019  ·  52Comments  ·  Source: vercel/next.js

Feature request

Currently there are a lot of users that create a custom server to do middleware behavior, for example server-side parsing of request bodies, cookies etc. Currently Next.js provides no way of handling this without a custom server. Meaning it can't be done in serverless / other environments.

Describe the solution you'd like

Ideally users would define middleware as a top level export. Meaning that pages can define middleware requirements:

// PageContext being the same values as `getInitialProps`'s context
export async function middleware({req, res}: PageContext) {
  // do something with `req` / `res`
}

function IndexPage() {
  return <h1>Hello world</h1>
}

export default IndexPage

However this also introduces the complexity of having to code-split / tree shake away user imports that are server-only.

So for the initial implementation (and incrementally working towards the implementation above) I'd start with supporting middleware in pages_document.js which is always server-side rendered so it makes sense to have it there.

One thing that was brought up is "why can't this just be done in getInitialProps of _document".

The reason that we need a new method is that the lifecycle of calling getInitialProps looks like this:

  • pages/_app.js getInitialProps is called,
  • pages/_app.js getInitialProps calls the page's getInitialProps
  • pages/_document.js getInitialProps is called
  • pages/_document.js getInitialProps calls renderPage
  • renderPage calls React's renderToString and returns the html, head tags and styles.
  • renderToStaticMarkup is called to render the _document shell
  • request is ended using send-html.ts, which adds etag etc.

Generally when using middleware it has to be called before getInitialProps because you could be parsing something needed in getInitialProps

So the middleware has to be called earlier.

Meaning the lifecycle would look like:

  • pages/_document.js middleware is called
  • pages/_app.js getInitialProps is called,
  • pages/_app.js getInitialProps calls the page's getInitialProps
  • pages/_document.js getInitialProps is called
  • pages/_document.js getInitialProps calls renderPage
  • renderPage calls React's renderToString and returns the html, head tags and styles.
  • renderToStaticMarkup is called to render the _document shell
  • request is ended using send-html.ts, which adds etag etc.

So for the initial implementation we'd want something like:

// pages/_document.js

// PageContext being the same values as `getInitialProps`'s context
export async function middleware({req, res}: PageContext) {
  // do something with `req` / `res`
}

// rest of _document.js

Most helpful comment

Hey @timneutkens and @Timer - great to see v9 has landed. I do believe that first-class support for API routes is a monumental and vital step for NextJs.

I've looked through #7297 and the new documentation, and it seems middleware support is not there yet, right? Would love to be able to rewrite next-i18next and remove the custom server requirement (https://github.com/isaachinman/next-i18next/issues/274).

All 52 comments

This new feature is aimed at supporting projects like next-i18next by default, which currently requires a custom server, cc @isaachinman

Relevant code: https://github.com/isaachinman/next-i18next/blob/master/src/middlewares/next-i18next-middleware.js#L52

What about React bugs that can be fixed ONLY by modifying the resulting HTML as a string?
I posted one earlier: https://github.com/zeit/next.js/issues/6718
I realize it raises additional concerns of streaming, etc. And it may be a rabbit hole. Nevertheless, let's at least discuss it to make us (consumers) aware of the official position at the moment. Should we have a pre and post stages or two different middlewares. Or it's just not worth it due to the extra complexity?

@timneutkens As discussed via Slack - just want to clarify that we're talking about vanilla req and res here, and Express-specific fields like req.body and req.query will _not_ be present. I imagine a lot of people are using Express (or similar) for middlewares, but what we're talking about here is a straight http implementation.

Is that correct?

@isaachinman yep!

@ivan-kleshnin I don't understand how this relates, you'd be able to override res.end in a similar manner as what you're already doing in that issue, even though I wouldn't recommend it.

Just a quick thought and potentially bikeshedding, but would it make sense to use the Request and Response primitives from the fetch-API? Seems like it could be nice to be able to use the same primitives across server, serviceworker and frontend code.

I'd rather stay with the Node.js req/res because:

  • It's what is passed to getInitialProps
  • It's compatible with most middlewares

@timneutkens Agree with that completely. I was going to mention - there'd probably be room after this feature lands to churn out little packages to enable 1:1 replacement for things like Express. Much better to begin with Node defaults.

♥️ it!

I've been using this workaround which allows me to add middlewares to getInitialProps():

See a demo of adding body-parser here: https://codesandbox.io/s/n3yj15mk7p

✅ Works for local dev/custom server (server.js)
✅ Works on target: 'serverless'
✅ Can parse anything (forms, JSON, etc)
✅ Is applied on a per-page basis
✅ Enables no-JS forms

Hey @timneutkens and @Timer - great to see v9 has landed. I do believe that first-class support for API routes is a monumental and vital step for NextJs.

I've looked through #7297 and the new documentation, and it seems middleware support is not there yet, right? Would love to be able to rewrite next-i18next and remove the custom server requirement (https://github.com/isaachinman/next-i18next/issues/274).

Our enterprise would also love to see this. We have an internal authentication system that requires a custom server currently and being able to move that logic to middleware and remove the custom server would dramatically simplify our project upgrading (we have hundreds of projects using Next - each major upgrade has had a decent amount of pain and pushback, other than next 9 - thanks for the backwards compat!).

Just my two cents here: Wouldn't make sense to be able to add a _middleware.js in the pages/api/ directory?

@zomars This is mostly relevant for actual frontend routes. It's already possible to use middlewares in your v9 API routes.

@isaachinman Is there a fully working example you can refer to?

@amardeep9911 I might be wrong, but one way is to wrap the handler function.
https://github.com/zeit/next.js/tree/canary/examples/api-routes-middleware

Hi @timneutkens, I'm testing the experimental functionality of middleware (great by the way) but I can never access the request object, when I log the middleware context I have this

> GET /                        
{                              
  err: undefined,              
  req: undefined,              
  res: undefined,              
  pathname: '/',               
  query: { amp: undefined },   
  asPath: '/',                 
  AppTree: [Function: AppTree] 
}                              

Should I create a separate issue? Or is that a normal restriction?

@HelloEdit I seem to remember that req/res are undefined unless your page has .getInitialProps() function, because without getInitialProps your page may be static

ho, thank you for your answer. but I sincerely hope that it is not the expected behavior: the middleware would lose all their interest if getInitialProps was required to make them work, or I missed a point 🤔
My interpretation of this is that the middleware will always run on the server side when the request is made.

@timneutkens nice, looking forward to when this gets implemented :) question for you. would this affect the automatic static optimizations outlined here?

It wouldn't work with static pages.

Hey @timneutkens, is the middleware also applied to API Routes or just non-api pages? Thanks!

hi guys,
I would like to chime in with the need for combining this with something that was referred in https://github.com/zeit/next.js/issues/9013

I'm personally of the opinion that nextjs should adopt expressjs and its middleware and not end up rebuilding a lot of stuff.
For example, we also use expressjs middleware to give seamless msgpack based responses (https://github.com/textbook/express-msgpack). A bunch of people do this for protocol buffers, etc as well.

there are tons of these kind of examples and i fear that nextjs will go through a long cycle rebuilding all of them (or expect end users to reimplement them)

@sandys In my opinion, adding too many things will make Next.js bloated, potentially hurting its performance and optimization. Also, not everyone's configuration is the same, so having several defaults may break things of those who don't use them. It is still safer to implement personally.

With all due respect, that's presumption that leveraging expressjs is going
to be less performant than doing it ourselves.

And yes - i agree that everyone's configuration is going to be
different..in fact so different, that it may make sense to leverage a vast
middleware plugin ecosystem that already exists.

I'm not opposed to a nextjs custom middleware. However I'm trying to put
forward a different view point.

On Mon, 21 Oct, 2019, 23:25 Hoang, notifications@github.com wrote:

@sandys https://github.com/sandys In my opinion, adding too many things
will make Next.js bloated, potentially hurting its performance and
optimization. Also, not everyone's configuration is the same, so having
several defaults may break things of those who don't use them. It is still
safer to implement personally.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/zeit/next.js/issues/7208?email_source=notifications&email_token=AAASYU4MXYPLGM2IBNHQHDTQPXUJXA5CNFSM4HJUGS7KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEB3G2YQ#issuecomment-544632162,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAASYU55YYIC6FZYIY7KOLDQPXUJXANCNFSM4HJUGS7A
.

@sandys I think you're missing the point.

Zeit.co is betting on serverless and one of the core feature of Next.js is serverless support. Express completely defeats this philosophy/architecture, as you need a centralised entrypoint/routing.

So you can be sure Express.js won't be used as a base (and it's why Next uses micro)

Using express is not needed for Next.js. But on the same note micro is not used for Next.js either.

Depending on the output target used the Node.js plain HTTP server is used which ensures the best performance (no other overhead). In the serverless target we just provide the req/res API as a low-level API.

Furthermore bundling express would mean the bundle size for serverless functions would be significantly larger.

At one point I was thinking these middlewares might also execute on the client but after reading this again, I am thinking the plan is to only execute these on the server. Is that a correct assumption?

@timneutkens thanks for replying - your answer is what i always assumed. At the end of the day, that is precisely the tradeoff you have to decide.

expressjs is 300-400kb (https://bundlephobia.com/[email protected]) . For those of us who dont do serverless, it unlocks a vast ecosystem of readymade middleware.

A middle ground (!!) is to maintain the expressjs API, but implement it ourselves. That way, Nextjs can provide a minimal size for serverless, while being able to plug into a large ecosystem for others.

@goldenshun correct. Note that at this point in time (few months are we release Next.js 9) I don't strongly believe this RFC is the way to go anymore. But I'm still thinking about it. Especially with plugins we could have a next-plugin-helmet (for security headers) and the other cases for middleware could be covered too.

@sandys I don't fully understand what you're trying to achieve on this thread. This RFC covers an edge case where people want to get rid of their custom server.

You can already run express if you want to as per the custom server API: https://github.com/zeit/next.js#custom-server-and-routing

Overall with Next.js 9 it's much less likely you'd need a custom server anyway.

I think there's still a good argument for middleware support that the current plugin RFC doesn't cover.

For example, something like next-plugin-helmet would need an additional plugin hook that can access and modify the Request and Response objects to use and set headers.

// on-request.js
const handler = helmet(config);

export default async function onRequest({ req, res }: { req: Request, res: Response }): void {
  // called upon receiving a request, allows you to modify response headers, e.g.:
  handler(req, res);
}

This proposed hook sounds like a good point for discussion generally, however, using environment variables as config isn't ideal, because helmet config can be complicated (nested objects) and use dynamic values based on the Request object:

const config = {
  contentSecurityPolicy: {
    scriptSrc: ["example.com", (req) => req.locals.nonce]
  }
}

This couldn't be configured with the nextjs key in the package.json of the plugin so would need an additional way of loading config for it to be a plugin.

Keen to hear thoughts 👍

@timneutkens I believe you are correct that the plugins system would cover many cases that could have been covered by a server-only middleware.

As I mentioned in my comment on the Custom Routes RFC, I am still on the hunt for a better solution to dynamic redirect logic than getInitialProps and hoped page level middleware that executes on both client and server could be a solution. The part that confuses people with redirects within getInitialProps is that there is no way to halt the page from rendering. We always need to have custom code that looks like this:

const Page = ({ isUserAuthenticated }) => {
  if (!isUserAuthenticated) return null; // bail since Page is going to render even when we call redirectToLogin.

  return (<div>...</div>);
}
Page.getInitialProps = async (ctx) => {
    if (!userIsAuthenticated(ctx) {
      logger.info('accessToken not found, redirecting user to login');
      redirectToLogin({ redirectTo: ctx.asPath, ctx });
      return {};
    }

    return { isUserAuthenticated: true };
  };

So I think I was looking for something similar to the original proposal in this thread:

export function middleware(ctx, next) {
  const { req, res } = ctx; // Same ctx that will eventually be passed to getInitialProps
  if(shouldRedirect(...)) {
    if (res) {
      res.redirect('/login');
    } else {
      Router.push('/login'); // also works on the client
    }
  }
  next(); // Similar to express, call the next middleware in the chain but only if we aren't redirecting
}

function IndexPage() {
  return <h1>Hello world</h1>
}

export default IndexPage

This is actually the _only_ use case I can think of that would take advantage of this style of middleware, so we might be able to narrow the scope of the problem by making this an explicit redirect function:

export function redirect(ctx) {
  if(shouldRedirect(ctx)) {
    return '/login';
  }
  return false; // null, undefined, etc. means no redirect
}

I personally think this makes a lot of sense since it will allow us to throw a whole bunch of external middlewares for express/koa/any other server environments. Some kind of precious uniformity with next ecosystem can be seen at the end of the tunnel :)

I strongly think this can leverage the case of having hard/heavy implementations on plugins and middlewares coming from all sort of ecosystems.

We use i18n and we're forced to do custom server, or serve the translations trough some lamdas from pages/api. It seems over engineered for me.

Cookies parsing, body parsing and some other middlewares already presents for api routes, could eventually be used here (opt-in, of course).

Some other practical cases are : check authentication tokens for every request, ab-testing redirections, etc.

I really hope this RFC comes out and help us all.

And thanks to all the zeit team for this amazing work, you're doing great, guys.

so as far as i understand the plugins rfc is kind of the v2 of this. alot of people are struggling with serverless i18n, @timneutkens is there anything you can say/share about this, maybe especially regarding support of next-i18next? https://github.com/isaachinman/next-i18next/issues/274

Would love to see a _middleware.js file that allow us add additional middleware to all req and res parameters (similar to express middlewares that are not page-specific). For example, a work around I've been using is to create a reusable middleware function that allows me to use compression, morgan, passport and cookie-sessions for all API requests:

import bodyParser from "body-parser";
import compression from "compression";
import morgan from "morgan";
import moment from "moment-timezone";
import passport from "passport";
import session from "cookie-session";
import applyMiddleware from "~middlewares/applyMiddleware";
import { sendError } from "~shared/helpers";

const { inProduction, cookieSecret } = process.env;

export default next => async (req, res) => {
    try {
        morgan.token("date", () => moment().format("MMMM Do YYYY, h:mm:ss a"));

        await applyMiddleware([
            compression({
                level: 6,
                filter: (req, res) =>
                    req.headers["x-no-compression"]
                        ? false
                        : compression.filter(req, res),
            }),
            session({
                path: "/",
                name: "app",
                maxAge: 2592000000, // 30 * 24 * 60 * 60 * 1000 expire after 30 days
                keys: [cookieSecret],
                httpOnly: true,
                // sameSite: inProduction, // specifies same-site cookie attribute enforcement
                // secure: inProduction,
            }),
            morgan(
                inProduction
                    ? ":remote-addr [:date] :referrer :method :url HTTP/:http-version :status :res[content-length]"
                    : "tiny",
            ),
            passport.initialize(),
            bodyParser.urlencoded({ extended: true }),
        ])(req, res);

        return next(req, res);
    } catch (error) {
        return sendError(error, res);
    }
};

In addition, use an additional middleware function to check session authentication:

import get from "lodash/get";
import { User } from "~models/instances";
import { sendError } from "~shared/helpers";
import { badCredentials } from "~shared/errors";

/**
 * Middleware function to check if a user is logged into a session and the session is valid.
 *
 * @async
 * @function
 * @param {object} - req
 * @param {object} - res
 * @returns {function}
 */
export default next => async (req, res) => {
    const _id = get(req, ["session", "id"]);
    if (!_id) return sendError(badCredentials, res);

    const existingUser = await User.findOne({ _id });
    if (!existingUser) return sendError(badCredentials, res);

    next(req, res);
};

And then I manually wrap my API route(s) with them:

import withMiddleware from "~middlewares";
import { User } from "~models/instances";
import { sendError } from "~shared/helpers";
import requireAuth from "~strategies/requireAuth";

/**
 * Retrieves logged in user app settings.
 *
 * @async
 * @function getProfile
 * @param {object} - req
 * @param {object} - res
 * @returns {res}
 */
const getProfile = async (req, res) => {
    try {
        const { id: _id } = req.session;

        const signedinUser = await User.findOne({ _id }, { password: 0, __v: 0 });
        if (!signedinUser) throw String("Unable to locate signed in user profile.");

        res.status(200).json({ signedinUser });
    } catch (err) {
        return sendError(err, res);
    }
};

export default withMiddleware(requireAuth(getProfile));

While it works, the downside is that every API page function needs to be manually wrapped with this withMiddleware function. Worse, is that this creates a callback hell, where additional middlewares have to be wrapped (as shown above with requireAuth) and they have to manually pass req and res to the next function... and so on. By exposing a _middleware.js config, one could attach middlewares to a stack and apply them to all future requests/responses (ideally once, instead of for each request) and, as a result, this should clean up some of the API setup redundancy.

@timneutkens Wanted to check why this RFC is still open. The changes at https://github.com/zeit/next.js/pull/7209 has been merged into canary. When will this be merged into master and available to use under Next.js 9.

It's unlikely this will land anytime soon as it introduces quite a bit of additional complexity for no additional gain based on upcoming features.

It's unlikely this will land anytime soon as it introduces quite a bit of additional complexity for no additional gain based on upcoming features.

Hope the team will suggest alternative for using middleware in client-side or if we can't use middleware at client-side at all!

Thanks!

This proposal didn't cover client-side middleware.

@timneutkens The current canary implementation involves maintaining a _document page, however, since the middleware is primarily focused on modifying the IncomingMessage and ServerResponse properties... can we just modify them directly within the Server class instead?

For example, I have a working prototype that utilizes the Server's nextConfig property to include some middleware specified in an applyMiddleware property placed within the next.config.js file. This property defines an array of middleware functions and an optional routes configuration option -- it'll conditionally apply the middleware to specific routes -- and is structured like so:

next.config.js

const bodyParser = require("body-parser");
const morgan = require("morgan");
const passport = require("passport");
const session = require("cookie-session");

const { cookieSecret } = process.env;

module.exports = {
  applyMiddleware: [
    [
      session({
        path: "/",
        name: "app",
        maxAge: 2592000000, // 30 * 24 * 60 * 60 * 1000 expire after 30 days
        keys: [cookieSecret],
        httpOnly: true,
      }), 
      { routes: ['/api'] }
    ]
    [ morgan('tiny'),  { routes: [ '/api', '/_error' ]  } ],
    passport.initialize(),
    bodyParser.urlencoded({ extended: true }),
  ],
}

Or, if you wanted to include your own middleware function(s), then that is also supported (all routes middleware or conditional routes middleware):

module.exports = {
  applyMiddleware: [
    (req, res) => {
      res.setHeader('all-routes-middleware', 'hello world!!!!')
    },
    [
      (req, res) => {
        res.setHeader('conditional-routes-middleware', 'bye world!!!!')
      },
      { routes: ['/api/user'] },
    ],
  ],
}

Then in the Server class, these middlewares are applied to the IncomingMessage and ServerResponse parameters within the run method.

The advantage of this approach is that all of this abstracted from the developer, it's opt-in, doesn't require a _document page, and works in both server and serverless environments.

Let me know what you think.

@mattcarlotta that won't work as Server is not used in all cases when using Next.js, eg in serverless. Similarly next.config.js is only loaded at dev and build time and we're not going to add options that depend on bootup to next.config.js. We're increasingly discouraging adding the config options that require runtime like publicRuntimeConfig etc so that we can eventually not require next.config.js on next start as people tend to load tons of modules that aren't needed in production (to improve bootup time for next start). On serverless we already only load next.config.js on dev/build.

Also there's no clear correct behavior with introducing those methods, for example Next.js apps are increasingly hybrid of SSG / SSR and middlewares won't be able to run for static files as they'd be served from an edge cache.

https://github.com/zeit/next.js/issues/7208#issuecomment-488317652

This case is covered by the rewrites/redirects and new data fetching methods btw.

Hey @timneutkens, I know this feature is still experimental but do you think is there any potential problem using this only to set some cookies. We only have SSR pages.

If you want to set cookies it's better to use API routes and redirect.

Could this middleware be used in addition of app.render() or nextRequestHandler() @timneutkens ? Like in the custom server's examples https://github.com/zeit/next.js/blob/canary/examples/custom-server-typescript/server/index.ts

I am asking this because our stack includes a headless CMS (Cockpit) and Next.js, doing the routing between the two is currently (really) hard (the routes being on the CMS, and slugs/URLs/pages could be added/removed/edited any time by an editor).

In our scenario, the pages/ folder are more like a views/ or templates/ folder, and we need to be able to render any page based on the data we get from the CMS.

Hey all! We just launched an NPM library for Express style architecture in Next.js without adding an Express server. It might be helpful for your middleware challenges! Check it out if you're interested. https://github.com/oslabs-beta/connext-js

@sarapowers I'm about to check this out - what's the minimum version of Next it can be used with? Thanks for sharing!

@runofthemill At this point, Connext utilizes Next.js's API routes so 9.0 is the minimum version you'd need. We'd love to someday make it compatible with older versions though!

Hey @timneutkens - just want to close the discussion off here with some clarifying points. There are two main reasons why next-i18next has been driven to use a custom server in the past:

  1. Language detection itself, based on cookies/headers/etc
  2. Redirects, based on language

The second issue is helped along by rewrites - just waiting for that to land from experimental.

However, we still need to execute language detection middleware for each incoming frontend request (and potentially 302 in some cases). If this RFC is no longer being considered, I'm wondering if you have a suggestions as to the "best practice" way to do that?

I've set forward https://github.com/isaachinman/next-i18next/pull/689 as an initial working idea of how to support serverless i18n with NextJs, and we're literally doing:

const middlewares = nextI18NextMiddleware(nextI18Next)
for (const middleware of middlewares) {
  await new Promise((resolve) => middleware(req, res, resolve))
}

Inside of a wrapped getInitialProps. Is this the user-land future of non-API middleware?

Current plan is to have the notion of languages / language detection built into Next.js, that way we can make it work with both SSG and SSR and have it work on the edge for the majority of cases. This will be similar to eg preview mode.

Ah, I wasn't aware that was on the cards.

Perhaps naively, I don't see how that would solve, or even simplify, the problem of localisation in NextJs apps.

Language detection itself is a solved problem. The real trick with universal/NextJs apps is handling language detection in conjunction with saved state via cookies, and potential state via routing (locale subpaths).

There may be ways to get close to fully-static solutions by taking opinionated approaches to routing, but many valid use cases will always require middleware.

Current plan is to have the notion of languages / language detection built into Next.js, that way we can make it work with both SSG and SSR and have it work on the edge for the majority of cases. This will be similar to eg preview mode.

Can we get more info on that @timneutkens ? In what aspects of I18n will Next.js be involved and in what it won't?

Can't share details yet. Still need to write up a RFC for it.

Was this page helpful?
0 / 5 - 0 ratings