routing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Routing & Proxy (Next.js 16)

路由与Proxy(Next.js 16)

1. High-Level Concept

1. 核心概念

This project MUST rely on the Next.js 16 Proxy (the successor to
middleware.ts
) to intercept requests at the edge. The Proxy's primary job is to enforce Authentication boundaries, perform Localization (i18n) redirects, and append security cookies/headers before a Page or Layout is ever rendered.
  • Edge Execution: The Proxy runs at the edge. It MUST NOT load Heavy Node modules or the Drizzle ORM directly.
  • Header Injection: You MUST append the
    x-current-path
    or
    x-locale
    to the requested headers so Server Components can read them later.
  • Cookies: You MUST use
    NextResponse.cookies
    strictly for high-level state like session identifiers and localization preferences.
本项目必须依赖Next.js 16 Proxy
middleware.ts
的替代方案)在边缘节点拦截请求。Proxy的主要职责是在页面或布局渲染前,强制执行认证边界、执行国际化(i18n)重定向,并添加安全Cookie/请求头。
  • 边缘执行:Proxy运行在边缘节点,禁止直接加载重型Node模块或Drizzle ORM。
  • 请求头注入:必须在请求头中添加
    x-current-path
    x-locale
    ,以便后续Server Components读取。
  • Cookie管理:必须严格使用
    NextResponse.cookies
    存储会话标识、本地化偏好等高层级状态。

2. Proxy Structure (Localization and Redirection)

2. Proxy结构(本地化与重定向)

You MUST export a
proxy
function instead of the legacy
middleware
function.
The most common responsibility is mapping the user's
locales
to the correct URL segment or redirecting them out of
dashboard
if they lack a session.
typescript
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { LOCALE, LOCALE_COOKIE_NAME } from "./shared/infrastructure/i18n/config";
import { isValidLocale, determineLocaleFromAcceptLangOrDefault } from "./shared/infrastructure/i18n/utils";

// Exclude static assets and api routes in config
export const config = {
  matcher: [`/((?!api|trpc|_next|_next/image|favicon.ico|_vercel|.*\\..*).*)`],
};

const pathWithoutLocale = ["dashboard"];

// 1. Helper to append locale headers
function createLocaleHeaders({ request, locale }: { request: NextRequest; locale: LOCALE; }) {
  const newHeaders = new Headers(request.headers);
  newHeaders.set(LOCALE_COOKIE_NAME, locale);
  return newHeaders;
}

// 2. Helper to forward request
function createNextResponse({ request, locale }: { request: NextRequest; locale: LOCALE; }) {
  const requestHeaders = createLocaleHeaders({ request, locale });
  const response = NextResponse.next({ request: { headers: requestHeaders } });
  response.cookies.set(LOCALE_COOKIE_NAME, locale, { httpOnly: true, path: "/" });
  return response;
}

// 3. Helper to redirect request
function createLocaleRedirect({ newPath, request, locale }: { newPath: string; request: NextRequest; locale: string; }) {
  const redirectUrl = new URL(newPath, request.url);
  const response = NextResponse.redirect(redirectUrl);
  response.cookies.set(LOCALE_COOKIE_NAME, locale, { httpOnly: true, path: "/" });
  return response;
}

export function proxy(request: NextRequest) {
  const { pathname, search } = request.nextUrl;
  const localeCookie = request.cookies.get(LOCALE_COOKIE_NAME)?.value;
  const segments = pathname.split("/");
  const firstSegment = segments[1];

  const preferredLocale = isValidLocale(localeCookie)
    ? localeCookie
    : determineLocaleFromAcceptLangOrDefault(request.headers.get("accept-language"));

  // Dashboards don't use i18n segments in URL
  if (pathWithoutLocale.includes(firstSegment)) {
    // Add authentication checks here
    return createNextResponse({ request, locale: preferredLocale });
  }

  // Segment matches exactly
  if (isValidLocale(firstSegment)) {
    if (firstSegment === preferredLocale) {
      return createNextResponse({ request, locale: preferredLocale });
    }
    // Redirect to preferred locale if wrong
    const newPath = pathname.replace(`/${firstSegment}`, `/${preferredLocale}`);
    return createLocaleRedirect({ newPath, request, locale: preferredLocale });
  }

  // Missing segment entirely
  const newPath = `/${preferredLocale}${pathname}${search}`;
  return createLocaleRedirect({ newPath, request, locale: preferredLocale });
}
必须导出
proxy
函数而非旧版的
middleware
函数。
最常见的职责是将用户的
locales
映射到正确的URL分段,或在用户未登录时将其从
dashboard
路由重定向出去。
typescript
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { LOCALE, LOCALE_COOKIE_NAME } from "./shared/infrastructure/i18n/config";
import { isValidLocale, determineLocaleFromAcceptLangOrDefault } from "./shared/infrastructure/i18n/utils";

// Exclude static assets and api routes in config
export const config = {
  matcher: [`/((?!api|trpc|_next|_next/image|favicon.ico|_vercel|.*\\..*).*)`],
};

const pathWithoutLocale = ["dashboard"];

// 1. Helper to append locale headers
function createLocaleHeaders({ request, locale }: { request: NextRequest; locale: LOCALE; }) {
  const newHeaders = new Headers(request.headers);
  newHeaders.set(LOCALE_COOKIE_NAME, locale);
  return newHeaders;
}

// 2. Helper to forward request
function createNextResponse({ request, locale }: { request: NextRequest; locale: LOCALE; }) {
  const requestHeaders = createLocaleHeaders({ request, locale });
  const response = NextResponse.next({ request: { headers: requestHeaders } });
  response.cookies.set(LOCALE_COOKIE_NAME, locale, { httpOnly: true, path: "/" });
  return response;
}

// 3. Helper to redirect request
function createLocaleRedirect({ newPath, request, locale }: { newPath: string; request: NextRequest; locale: string; }) {
  const redirectUrl = new URL(newPath, request.url);
  const response = NextResponse.redirect(redirectUrl);
  response.cookies.set(LOCALE_COOKIE_NAME, locale, { httpOnly: true, path: "/" });
  return response;
}

export function proxy(request: NextRequest) {
  const { pathname, search } = request.nextUrl;
  const localeCookie = request.cookies.get(LOCALE_COOKIE_NAME)?.value;
  const segments = pathname.split("/");
  const firstSegment = segments[1];

  const preferredLocale = isValidLocale(localeCookie)
    ? localeCookie
    : determineLocaleFromAcceptLangOrDefault(request.headers.get("accept-language"));

  // Dashboards don't use i18n segments in URL
  if (pathWithoutLocale.includes(firstSegment)) {
    // Add authentication checks here
    return createNextResponse({ request, locale: preferredLocale });
  }

  // Segment matches exactly
  if (isValidLocale(firstSegment)) {
    if (firstSegment === preferredLocale) {
      return createNextResponse({ request, locale: preferredLocale });
    }
    // Redirect to preferred locale if wrong
    const newPath = pathname.replace(`/${firstSegment}`, `/${preferredLocale}`);
    return createLocaleRedirect({ newPath, request, locale: preferredLocale });
  }

  // Missing segment entirely
  const newPath = `/${preferredLocale}${pathname}${search}`;
  return createLocaleRedirect({ newPath, request, locale: preferredLocale });
}

3. Route Layouts (Public vs Private)

3. 路由布局(公开 vs 私有)

If the project is multilingual (i18n), you MUST cleanly separate pages into routes that contain the
[locale]
segment in the URL (public) and those that do not (private/authenticated). A proxy handles the boundary between them.
  1. app/[locale]/
    (Public Routes):
    • You MUST place public paths like the root landing page,
      /login
      ,
      /register
      , or
      /pricing
      here.
    • The UI automatically wraps inside the localized context derived from the URL parameters instead of headers.
    • You MUST NOT fetch the session aggressively from use-cases unless your explicit goal is to redirect an already-logged-in user to an authenticated route.
  2. app/dashboard/
    (Private / Authenticated Routes):
    • You MUST NOT include
      [locale]
      in the URL segment for private, authenticated functionality.
    • It MUST rely entirely on the
      x-locale
      cookie injected by the Proxy to determine language translation for components.
    • Auth validation MUST be the primary responsibility of
      serviceContainer
      use-cases called in this layout, not the Proxy, to preserve Domain rules.
如果项目支持多语言(i18n),必须将页面清晰划分为URL中包含
[locale]
分段的路由(公开)和不包含该分段的路由(私有/已认证)。Proxy负责处理两者之间的边界。
  1. app/[locale]/
    (公开路由):
    • 必须将根着陆页、
      /login
      /register
      /pricing
      等公开路径放置在此处。
    • UI会自动包裹在从URL参数获取的本地化上下文中,而非依赖请求头。
    • 除非明确要将已登录用户重定向到认证路由,否则不要在用例中主动获取会话信息。
  2. app/dashboard/
    (私有/已认证路由):
    • 私有、已认证功能的URL分段中禁止包含
      [locale]
    • 必须完全依赖Proxy注入的
      x-locale
      Cookie来确定组件的语言翻译。
    • 认证验证必须是此布局中调用的
      serviceContainer
      用例的主要职责,而非Proxy,以保留领域规则。

4. Resource Routing (IDs vs Slugs)

4. 资源路由(ID vs 别名)

When constructing dynamic URL structures, particularly in a REST API-like hierarchy, you MUST use stable entity Identifiers (IDs), NOT slugs or entity names. Names can change over time via user edits, which causes slugs to break bookmarks and historical references.
  • Correct:
    /dashboard/[workspaceId]/folders/[folderId]
  • Incorrect:
    /dashboard/[workspaceSlug]/folders/[folderSlug]
构建动态URL结构时,尤其是在类REST API的层级中,必须使用稳定的实体标识符(ID),而非别名(slugs)或实体名称。名称可能会随用户编辑而更改,导致别名破坏书签和历史引用。
  • 正确示例
    /dashboard/[workspaceId]/folders/[folderId]
  • 错误示例
    /dashboard/[workspaceSlug]/folders/[folderSlug]

5. Contextless Route Redirection

5. 无上下文路由重定向

Any route that cannot render intelligently without context MUST NOT attempt to load a generic UI. It MUST act exclusively as a redirection layer.
For example, in a multi-tenant or workspace-based application, navigating to
/dashboard
directly lacks the necessary
{ workspaceId }
routing parameter to display relevant details. Therefore,
/dashboard/page.tsx
MUST NOT exist as a rendered page. Instead, it MUST execute a Use Case to identify the user's primary or last-used entity and immediately
redirect()
to the proper context path, such as
/dashboard/[workspaceId]
.
任何缺乏上下文就无法智能渲染的路由,都不得尝试加载通用UI,必须仅作为重定向层。
例如,在多租户或基于工作区的应用中,直接访问
/dashboard
缺少必要的
{ workspaceId }
路由参数来显示相关详情。因此,
/dashboard/page.tsx
不得作为可渲染页面存在。相反,它必须执行一个用例来识别用户的主要或最近使用的实体,并立即
redirect()
到正确的上下文路径,如
/dashboard/[workspaceId]

6. Protected Paths and Server Components

6. 受保护路径与Server Components

You MUST NOT throw
Forbidden
or natively redirect inside a Server Component layout unless exhausting
assertNever
domain errors. While the proxy CAN intercept unauthorized navigation statically, complex auth rules (e.g., "Does this user own Workspace X?") MUST be resolved defensively in the Use Case executed by a Server Component.
除非遇到
assertNever
领域错误,否则不得在Server Components布局中抛出
Forbidden
错误或原生重定向。虽然Proxy可以静态拦截未授权导航,但复杂的认证规则(如“该用户是否拥有工作区X?”)必须由Server Components执行的用例来防御性地解决。

7. References

7. 参考资料

For practical examples of routing and proxy patterns:
  • Server Component Domain Redirect - How to properly handle complex domain-based auth and redirection inside a Next.js Layout/Page without depending on the Proxy.
  • Contextless Redirect - How to handle root authenticated paths (e.g.,
    /dashboard
    ) that lack context parameters and MUST redirect to specific resource URLs.
有关路由和Proxy模式的实用示例:
  • Server Component Domain Redirect - 如何在Next.js布局/页面中正确处理基于域名的复杂认证和重定向,而不依赖Proxy。
  • Contextless Redirect - 如何处理缺少上下文参数的根认证路径(如
    /dashboard
    ),并将其重定向到特定资源URL。