Protect Routes using the Middleware

What you'll cover in this guide

  • ✅ Create an API endpoint to protect
  • ✅ Add the MonoCloud middleware to the Next.js app
  • ✅ Explore ways to specify which routes to protect
  • ✅ Run a simple demo to see it in action
Using the Next.js App Router instead? See the App Router version of this guide.

Before you get started

This guide assumes you have a Next.js project set up and running. If you haven't set up authentication in your Next.js Page Router project yet, please refer to the quickstart guide.

Create a protected API

First, we need to create the API endpoint we want to protect with MonoCloud.

Inside the directory src/pages/api, create a file named protected.ts and insert the following code:

src/pages/api/protected.ts
import { NextApiRequest, NextApiResponse } from "next";

export default function Protected(req: NextApiRequest, res: NextApiResponse) {
  return res.json({ secret: "The secret" });
}

This endpoint will return a JSON response with the payload: { "secret": "The secret" }.

Add the MonoCloud Middleware

Create a file named middleware.ts in the src directory and enter the following code:

src/middleware.ts
import { monoCloudMiddleware } from "@monocloud/nextjs-auth";

export default monoCloudMiddleware();

export const config = {
  matcher: ['/', '/(api)(.*)', '/((?!.*\\..*|_next).*)'],
};
The matcher configuration ensures that the middleware will only process requests for main user-facing pages and API routes, while excluding static files and Next.js-specific resources.

By default, the MonoCloud middleware protects all routes in your application. Later, we'll see how to customize this behavior to protect only specific routes.

Fetch data from the API and display it

Now that we have an endpoint, let's see it in action. We'll trigger a call to the protected API when a button is clicked.

Open src/pages/index.tsx and replace the existing code with:

src/pages/index.tsx
import { SignIn, SignOut, useUser } from "@monocloud/nextjs-auth/client";
import { useState } from "react";

export default function FetchData() {
  const { isAuthenticated } = useUser();
  const [data, setData] = useState();

  const handleFetch = async (url: string) => {
    const response = await fetch(url);
    setData(await response.json());
  };

  return (
    <div className="flex flex-col gap-3 max-w-sm">
      <div className="flex flex-col gap-1">
        <p>Data from route: {JSON.stringify(data, undefined, 2)}</p>
      </div>
      <button className="outline p-2" onClick={() => handleFetch("/api/protected")}>
        Fetch Protected Data
      </button>
      {isAuthenticated ? <SignOut>Sign Out</SignOut> : <SignIn>Sign In</SignIn>}
    </div>
  );
}

Let's review what this code does:

  1. When the Fetch Protected Data button is clicked, it triggers the handleFetch() function.
  2. The handleFetch() function calls the /api/protected endpoint we created earlier.
  3. The response body text is stored in the state.
  4. Then we display the stored data in a <div> above the button.

See it in action by running:

Terminal
npm run dev

Then visit http://localhost:3000.

Since we've protected all routes, including the root, you'll be redirected to the sign-in page.

Once you're signed in, click the Fetch Protected Data button. You should see the secret data retrieved from the protected API: { "secret": "The Secret." }.

Choosing which routes to protect

It's likely that you'll want to protect some routes while leaving others open to everyone. For example, you might have a public homepage or a contact page that doesn't require authentication.

We can configure monoCloudMiddleware() to protect specific routes selectively.

The SDK gives you multiple ways to configure which routes to protect:

  • Provide a list of routes to protect
  • Pass a custom function to create a dynamic list of protected routes
  • Create your own middleware that runs before monoCloudMiddleware()

Let's look at each one in turn.

Provide a list of routes to protect

To protect specific routes, you can configure the protectedRoutes option in the monoCloudMiddleware(). The option accepts an array of route identifiers.

For example, if you want to protect the /api/protected endpoint, while leaving everything else publicly accessible, you'd pass protectedRoutes: ["/api/protected"] as an option to the monoCloudMiddleware():

src/middleware.ts
import { monoCloudMiddleware } from "@monocloud/nextjs-auth";

export default monoCloudMiddleware({ protectedRoutes: ["/api/protected"] });

export const config = {
  matcher: ['/', '/(api)(.*)', '/((?!.*\\..*|_next).*)'],
};

To get more control over the routes you want to protect, you can also use regular expressions in the array you pass to protectedRoutes.

For example, to protect all routes under /api/protected, you could use the regex pattern ^/api/protected(/.*)?$ This pattern will match any route that starts with /api/protected.

src/middleware.ts
import { monoCloudMiddleware } from "@monocloud/nextjs-auth";

export default monoCloudMiddleware({ protectedRoutes: ["^/api/protected(/.*)?$"] });

export const config = {
  matcher: ['/', '/(api)(.*)', '/((?!.*\\..*|_next).*)'],
};
When the protectedRoutes option is set to an empty array, all routes are unprotected.

Using a custom function

You can protect routes dynamically by providing a custom function to monoCloudMiddleware() instead of a static list of routes. This callback function provides the incoming request as an argument, enabling you to decide which routes to secure based on the details of the request.

For example, you could protect all routes under /api/protected by checking if the request URL contains that path:

src/middleware.ts
import { monoCloudMiddleware } from "@monocloud/nextjs-auth";

export default monoCloudMiddleware({
  protectedRoutes: (req) => {
     // Your logic
    return req.nextUrl.pathname.startsWith('/api/protected');
  },
});

export const config = {
  matcher: ['/', '/(api)(.*)', '/((?!.*\\..*|_next).*)'],
};

Return monoCloudMiddleware()

If your application requires complex logic to determine which routes to protect, you can create a custom middleware that conditionally runs the monoCloudMiddleware(). This approach also allows you to combine multiple middlewares.

src/middleware.ts
import { monoCloudMiddleware } from "@monocloud/nextjs-auth";
import { NextFetchEvent, NextRequest, NextResponse } from "next/server";

export default function customMiddleware(req: NextRequest, evt: NextFetchEvent) {
  if (req.nextUrl.pathname.startsWith("/api/protected")) {
    return monoCloudMiddleware(req, evt);
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/', '/(api)(.*)', '/((?!.*\\..*|_next).*)'],
};

In this example, the customMiddleware() function conditionally runs the monoCloudMiddleware(). When a route meets certain criteria (for instance, any route beginning with /api/protected), the monoCloudMiddleware() is activated to manage authentication.

For other routes, the middleware allows the request to proceed without any additional checks by returning NextResponse.next(). This ensures that authentication checks are only performed where necessary.

Add an unprotected route

By default, the monoCloudMiddleware() protects all routes. However, when you specify particular routes to protect using one of the three methods described earlier, any routes not explicitly listed will remain unprotected. This approach allows you to secure specific parts of your application while leaving others accessible to all users.

We can put this to the test by creating a new route that will be unprotected by the middleware.

First, create a file named route.ts under src/pages/api/unprotected and add the following code:

src/pages/api/unprotected.ts
import { NextApiRequest, NextApiResponse } from "next";

export default function Unprotected(req: NextApiRequest, res: NextApiResponse) {
  return res.json({ message: "Open access" });
}

The /api/unprotected endpoint returns { "message": "Open access" } as the response body.

Next, add a second button to fetch data from /api/unprotected. In your src/pages/index.tsx file, place this new button next to the existing Fetch Protected Data button:

src/pages/index.tsx
import { SignIn, SignOut, useUser } from "@monocloud/nextjs-auth/client";
import { useState } from "react";

export default function FetchData() {
  const { isAuthenticated } = useUser();
  const [data, setData] = useState();

  const handleFetch = async (url: string) => {
    const response = await fetch(url);
    setData(await response.json());
  };

  return (
    <div className="flex flex-col gap-3 max-w-sm">
      <div className="flex flex-col gap-1">
        <p>Data from route: {JSON.stringify(data, undefined, 2)}</p>
      </div>
      <button className="outline p-2" onClick={() => handleFetch("/api/protected")}>
        Fetch Protected Data
      </button>
      <button className="outline p-2" onClick={() => handleFetch("/api/unprotected")}>
        Fetch Unprotected Data
      </button>
      {isAuthenticated ? <SignOut>Sign Out</SignOut> : <SignIn>Sign In</SignIn>}
    </div>
  );
}

The second button will fetch data from the /api/unprotected route and display it on the page.

Fire up your Next.js application by running:

Terminal
npm run dev

Once the app is running, visit http://localhost:3000. This time, you won't be prompted to sign in because we've explicitly protected some routes, leaving all others unrestricted.

If you fetch data from the unprotected route /api/unprotected, you'll see the message { "message": "Open access" }.

However, if you're not signed in and attempt to fetch data from a protected route, you'll see the message { "message": "unauthorized" }.

Try it out and then sign in by clicking the Sign In button. Once you're back at the Next.js application, click the button to fetch data from the protected route. This time, you'll see the data { "secret": "The secret" }.

And that's how you can use the monoCloudMiddleware() to protect specific routes in your Next.js application.