npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@ohm-vision/next-middleware

v1.0.1

Published

A composer library to support NextJS middlewares from version 13 forward

Downloads

142

Readme

next-middleware

Wrapper for NextJS App Router middleware

This wrapper will allow for middleware composition

Note: This library has only been tested in Next v14+

YOU PROBABLY DON'T NEED THIS LIBRARY

If you just have a single middleware, you don't need this

If you are doing complicated work in your middleware, you don't need this

npm version

"Buy Me A Coffee"

Installation

Run the following command

npm install @ohm-vision/next-middleware

Usage

NextJS Documentation

Create a middleware.ts or middleware.js file in your project directory (be sure to place it in the src directory if used)

Import the composeMiddlewares function from @ohm-vision/next-middleware

The function accepts a list of either middleware functions or middleware with matcher configurations

Middleware are invoked in the order they are registered. The first middleware to return an instance of NextResponse or Response will short-circuit and break the chain.

Just as in the docs, middlewares are invoked for every request including next requests to fetch static assets. The global config object you export will define all routes which the child middlewares should listen for. If you want the composer to handle all paths, you can either remove the config entirely, or specify the source as just /

The composer will attempt to "compile" all of the registered middlewares at build-time into a single executing function vs resolve complex configurations dynamically.

To support nesting and fallthrough, a default NextResponse.next() object is created prior to all middleware's being run and is passed to each middleware. This will allow you to enrich the response step-by-step vs having one large middleware handle injecting custom locale, theme, or other bits.

If you would like to nest middleware execution (not recommended), you can call the composeMiddlewares multiple times as deeply as you'd like. Although I seriously recommend keeping your middleware "tree" as shallow as possible

If you are using middleware for authenticating the user session (such as with next-auth), you'll notice that the composeMiddlewares function has a type argument to set it to the NextRequest-like type which they support. You can also extend this on your own to add additional properties to the request object such as data to be shared by other middlewares

Next Middleware

Each middleware will be passed the following:

  • arg0: (object) - this is the unified object containing
    • req: NextRequest - original NextRequest (cast to whatever type you choose)
    • res: NextResponse - default NextResponse object
    • evt: NextFetchEvent - original NextFetchEvent
  • arg1: NextFetchEvent - original NextFetchEvent

Next Middleware With Matcher

We follow the NextJS documentation and support all properties and types except for locale.

Important Note: I had no choice but to omit the locale property when in this compose mode as I really have no idea how to facilitate that NextJS magic dynamically. If you have any ideas, please feel free to open a PR

Additionally, I've added support for a dynamic function to return a boolean for more customized middleware matching

I'd probably only recommend using something like this if you're trying to reduce the number of times the path matching is done (ie. a bunch of middleware only runs when authenticated)

I have no doubt there will be a performance impact doing this work relatively dynamically so use them SPARINGLY

Example

//- @/middlewares/locale.middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { NextMiddlewareProps } from "@ohm-vision/next-middleware";

import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'

import defaultLocale, { locales } from "../i18n";

import cookieConfig from "../config/cookie.config";
import headerConfig from "../config/header.config";

export function LocaleMiddleware({ req: { headers, cookies }, res }: NextMiddlewareProps) {
    let locale;

    // Priority 1: Use existing cookie
    if (!locale && cookies && cookies.has(cookieConfig.locale)) {
        const value = cookies.get(cookieConfig.locale)?.value;

        if (value && locales.includes(value)) {
            locale = value;
        }
    }

    // Priority 2: Use `accept-language` header
    if (!locale && headers && headers.has(headerConfig.acceptLanguage)) {
        const languages = new Negotiator({
            headers: {
                [headerConfig.acceptLanguage]: headers.get(headerConfig.acceptLanguage)
            }
        }).languages();
        
        try {
            locale = match(languages, locales, defaultLocale);
        } catch {
            // Invalid language
        }
    }

    // Priority 3: Use default locale
    if (!locale) {
        locale = defaultLocale;
    }

    res.headers.set(headerConfig.locale, locale);
}

//- @/middlewares/theme.middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { NextMiddlewareProps } from "@ohm-vision/next-middleware";

import defaultTheme, { isThemeName } from "../themes/types/theme-names.type";

import cookieConfig from "../config/cookie.config";
import headerConfig from "../config/header.config";

export function ThemeMiddleware({ req: { headers, cookies }, res }: NextMiddlewareProps) {
    let theme;

    // Priority 1: Use existing cookie
    if (!theme && cookies && cookies.has(cookieConfig.theme)) {
        const value = cookies.get(cookieConfig.theme)?.value;

        if (value && isThemeName(value)) {
            theme = value;
        }
    }

    // Priority 2: Use `sec-ch-prefers-color-scheme` header
    if (!theme && headers && headers.has(headerConfig.secChPrefersColorScheme)) {
        const value = headers.get(headerConfig.secChPrefersColorScheme);

        if (value && isThemeName(value)) {
            theme = value;
        }
    }

    // Priority 3: Use default
    if (!theme) {
        theme = defaultTheme;
    }

    res.headers.set(headerConfig.theme, theme);
}

//- @/middlewares/analytics.middleware.ts
import { NextMiddlewareProps } from "@ohm-vision/next-middleware";
 
export function AnalyticsMiddleware({ req, evt }: NextMiddlewareProps) {
  evt.waitUntil(
    fetch('https://my-analytics-platform.com', {
      method: 'POST',
      body: JSON.stringify({ pathname: req.nextUrl.pathname }),
    })
  );
}

//- @/middlewares/cors.middleware.ts
import { NextRequest, NextResponse } from 'next/server'
 
const allowedOrigins = ['https://acme.com', 'https://my-app.org']
 
const corsOptions = {
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
}
 
export function CorsMiddleware({ req, res }: NextMiddlewareProps) {
  // Check the origin from the request
  const origin = req.headers.get('origin') ?? ''
  const isAllowedOrigin = allowedOrigins.includes(origin)
 
  // Handle preflighted requests
  const isPreflight = req.method === 'OPTIONS'
 
  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { 'Access-Control-Allow-Origin': origin }),
      ...corsOptions,
    }
    return NextResponse.json({}, { headers: preflightHeaders })
  }
 
  // Handle simple requests
  if (isAllowedOrigin) {
    res.headers.set('Access-Control-Allow-Origin', origin)
  }
 
  Object.entries(corsOptions).forEach(([key, value]) => {
    res.headers.set(key, value)
  });
}

//- @/middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { composeMiddleware } from "@ohm-vision/next-middleware";

import { LocaleMiddleware } from "@/middlewares/locale.middleware.ts";
import { AnalyticsMiddleware } from "@/middlewares/analytics.middleware.ts";
import { ThemeMiddleware } from "@/middlewares/theme.middleware.ts";
import { CorsMiddleware } from "@/middlewares/cors.middleware.ts";

export const middleware = composeMiddleware(
    {
        // this middleware will only fire for API routes
        middleware: CorsMiddleware
        matcher: '/api/:path*'
    },
    AnalyticsMiddleware,
    LocaleMiddleware,
    ThemeMiddleware,
    {
        // experimentally nest middleware which share the same matcher
        matcher: "/dashboard",
        // customized request object which the AuthMiddleware will enrich
        middleware: composeMiddleware<NextRequest & {
            auth: {
                roles: string[]
            }
        }>(
            async ({ req }) => {
                const { cookies } = req;

                if (!cookies.has("Session")) return NextResponse.redirect("/login");

                const session = cookies.get("Session");

                // todo: validate session in db, or decode/validate JWT

                // todo: assign roles based on the JWT
                req.auth.roles = ["blogs"];
            },
            {
                // dynamic matcher function
                matcher: ({ req }) => req.pathname.startsWith("/dashboard/blogs"),
                middleware: async ({ req }) => {
                    // if the user does not have the "blogs" role, redirect them to a restricted error page
                    if (!req.auth.roles.includes("blogs")) {
                        return NextResponse.redirect("/dashboard/restricted");
                    }
                }
            }


        )
    }
    //- ... and many more
);

export const config = {
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico, sitemap.xml, robots.txt (metadata files)
     */
    matcher: "/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)"
};

Contact Me

Ohm Vision, Inc