How can I enforce server-side step completion before allowing access to a route in Next.js?

1 week ago 10
ARTICLE AD BOX

You need to use a proxy in Next.js (V16.0.0+). But as in nodejs environment, we can directly call the database for a response, in NextJS proxy, we can't do this as the proxy runs on edge and the database requires nodejs environment. Instead, we can solve the problem using two ways.

Create a new route where the database connection is done, then use the proxy to call the API and check the access.

API code:

// /pages/api/check-access/routes.ts import { NextResponse } from 'next/server' import { db } from '@/lib/db' // Your DB setup export async function GET(request) { // 1. Validate the user (e.g., from session cookie) const userId = getUserIdFromRequest(request) if (!userId) { return NextResponse.json({ allowed: false }, { status: 401 }) } // 2. Perform your DB check here (Node.js runtime) const hasCompletedStep = await db.userSteps.isComplete(userId) return NextResponse.json({ allowed: hasCompletedStep }) }

Proxy code (add this to the root):

//proxy.ts import { NextResponse } from 'next/server' export async function middleware(request) { // 1. Construct the internal API URL const checkUrl = new URL('/api/check-access', request.url) // 2. Forward the cookies so the API knows who the user is or send a bearer token(JWT) const headers = { cookie: request.headers.get('cookie') || '', } // 3. Fetch the check (Wait for the API response) const response = await fetch(checkUrl, { headers }) const data = await response.json() // 4. LOGIC: If allowed, Proxy. If not, Redirect. if (data.allowed) { // User is good: Proxy to the dashboard view const targetUrl = new URL('/dashboard', request.url) return NextResponse.rewrite(targetUrl) } else { // User is blocked: Redirect to the "Complete Steps" page return NextResponse.redirect(new URL('/steps-incomplete', request.url)) } } ///dashboard/:path* -> Match /dashboard and any sub-paths (e.g., /dashboard/settings) export const config = { matcher: '/dashboard', }

Use a cookie flag.

When the user completes step, the backend writes a cookie step_completed=true (preferably signed/encrypted).
Proxy: Simply checks for the cookie. No database call needed.

Proxy code:

import { NextResponse } from 'next/server' export function middleware(request) { // Check cookie directly (Instant, no DB lag) const hasStepCookie = request.cookies.get('step_completed') if (hasStepCookie?.value === 'true') { return NextResponse.rewrite(new URL('/internal-dashboard-view', request.url)) } else { return NextResponse.redirect(new URL('/steps-incomplete', request.url)) } } // This limits the middleware to ONLY run on these paths. export const config = { matcher: [ // Match exactly /dashboard '/dashboard', // Match anything starting with /dashboard/ (e.g., /dashboard/settings) '/dashboard/:path*', // You can add more unrelated paths here '/account/:path*' ], }

Recommendation: If the "step completion" is critical security (e.g., payment), use Option 1 or verify again on the destination page. If it is just user flow (e.g., onboarding), use Option 2.

If unclear then I would love to help you out 😊.

References: https://nextjs.org/docs/app/api-reference/file-conventions/proxy

Read Entire Article