Next.js 混合路由踩坑:加了 `pages/api` 之后的两个隐藏问题

Next.js 混合路由踩坑:加了 pages/api 之后的两个隐藏问题

项目用的是 Next.js App Router,但有几个接口需要放在 pages/api 下------某些第三方回调必须走 Pages Router,另外想单独暴露几个内部接口供后端调用。

官方说两套路由可以共存,实际落地踩了两个坑,网上几乎搜不到答案,记录备查。

环境

  • Next.js 16(App Router 主体 + pages/api 混用)
  • TypeScript 5.x

现象

项目里加了第一个 pages/api/xxx.ts 文件之后,没改任何其他代码,CI 突然炸了一堆 TS 报错:

python 复制代码
TS18047: 'pathname' is possibly 'null'.
TS2345: Argument of type 'string | null' is not assignable to parameter of type 'string'.

涉及所有用到 useParamsusePathnameuseSearchParams 的地方。跑起来完全正常,是纯粹的类型误报。

根因

Next.js 检测到 pages/ 目录存在时,会在自动生成的 next-env.d.ts 里额外引入一份 compat 类型声明:

bash 复制代码
node_modules/next/navigation-types/compat/navigation.d.ts

这份文件的设计初衷是为了让组件能在 App Router 和 Pages Router 同时存在 时兼容两套路由------在 Pages Router 上下文里这些 Hook 确实可能返回 null,所以它把返回值都加了 | null

ts 复制代码
// compat/navigation.d.ts(简化)
export function usePathname(): string | null;
export function useSearchParams(): ReadonlyURLSearchParams | null;
export function useParams<T>(): T | null;

在纯 App Router 上下文里,这几个 Hook 运行时不可能返回 null。compat 声明把类型拓宽了,所有下游使用都被迫加判空。

注意 :这是 Next.js 未在官方文档中说明的内部行为,只能从源码 node_modules/next/navigation-types/compat/navigation.d.ts 里看到。

解法

新建 types/next-navigation-override.d.ts,用 TypeScript 模块增量声明把返回类型覆盖回非空:

ts 复制代码
import type { ReadonlyURLSearchParams } from 'next/navigation';

/**
 * 覆盖 Pages Router compat 类型对 App Router navigation hooks 的 nullable 污染。
 * next-env.d.ts 会引入 compat/navigation.d.ts,将返回类型拓宽为 | null。
 * App Router 下这些 hooks 运行时不会返回 null,在此恢复原始非空签名。
 */
declare module 'next/navigation' {
  export function useSearchParams(): ReadonlyURLSearchParams;
  export function usePathname(): string;
  export function useParams<
    T extends Record<string, string | string[]> = Record<string, string | string[]>
  >(): T;
  export function useSelectedLayoutSegments(): string[];
  // useSelectedLayoutSegment 在无匹配 segment 时确实返回 null,保留
  export function useSelectedLayoutSegment(): string | null;
}

确保 tsconfig.jsoninclude 覆盖到 types/ 目录即可。运行时零影响,纯类型层面修正。

注意useSelectedLayoutSegment 在真正没有匹配的 layout segment 时会返回 null,语义上是正确的,不要一起覆盖成非空。


坑二:rewrites 数组写法会把 pages/api 路由直接代理出去

现象

开发环境配了 rewrites,把 /api/:path* 代理到后端服务。加了 pages/api/internal/* 之后,这些接口全都被代理走了,本地 handler 从未执行过。

根因

Next.js 的 rewrites 有两种写法,行为完全不同(官方文档):

写法一:直接返回数组

ts 复制代码
async rewrites() {
  return [
    { source: '/api/:path*', destination: 'https://backend.example.com/api/:path*' }
  ];
}

这种写法等价于 beforeFiles------在 Next.js 解析文件系统路由之前 就匹配。所以 pages/api/internal/* 还没被识别,请求就已经被代理出去了。

Next.js 完整的路由匹配顺序是:

  1. Headers
  2. beforeFiles rewrites
  3. 静态文件(public/_next/static、pages 文件)
  4. afterFiles rewrites
  5. fallback rewrites → 最终 404

写法二:返回带阶段的对象

ts 复制代码
async rewrites() {
  return {
    beforeFiles: [],  // 文件系统路由之前匹配
    afterFiles: [],   // 文件系统路由之后匹配,找不到才走这里
    fallback: []      // 最后兜底
  };
}

解法

把后端代理规则挪到 afterFiles,让 Next.js 先跑自己的 pages/api/*,匹配不到才转发后端:

ts 复制代码
async rewrites() {
  const staticRewrites = [
    { source: '/', destination: '/home' },
    { source: '/:locale(en|fr|ja)', destination: '/:locale/home' }
  ];

  if (process.env.NODE_ENV !== 'development') {
    return staticRewrites;
  }

  return {
    beforeFiles: [],
    // afterFiles:Next.js 自己的 pages/api/* 优先,其余 /api/* 才代理到后端
    afterFiles: [
      ...staticRewrites,
      {
        source: '/api/:path*',
        destination: 'https://backend.example.com/api/:path*'
      }
    ],
    fallback: []
  };
}

改完之后,/api/internal/blog/render/detail 由本地 handler 处理,/api/user/info 等没有本地文件的路径才走代理。


总结

问题 触发条件 表现 解法
navigation hook 类型被 nullable 化 添加了任意 pages/ 文件 大量 TS18047 / TS2345 误报 模块增量声明覆盖 compat 类型
pages/api 路由被 rewrite 拦截 同时配置了 /api/* 代理 本地 handler 从未执行 代理规则改放 afterFiles

两个坑的共同特点:都是 Next.js 在混合模式下静默注入的行为,不翻源码或文档边角很难定位原因。

如果你也在用 App Router + pages/api 的混合方案,遇到过其他坑欢迎评论区交流。


参考

相关推荐
阿珊和她的猫3 小时前
使用 TypeScript 实现数组类型判断方法
javascript·typescript·状态模式
落魄江湖行7 小时前
基础篇八 Nuxt4 中间件进阶:请求拦截与权限校验
前端·typescript·nuxt4
zero15979 小时前
TypeScript 快速实战系列:核心进阶|接口(Interface) + 类型(Type):大模型开发神器
前端·typescript·大模型编程语言
落魄江湖行9 小时前
基础篇九 Nuxt4 插件系统:扩展 Nuxt 能力
前端·vue.js·typescript·nuxt4
comerzhang6551 天前
return null:Next.js App Router 博客的 14 个 SEO 死穴
next.js
Ava的硅谷新视界1 天前
TypeScript 中用判别联合类型替代 instanceof 检查
前端·javascript·typescript
落魄江湖行1 天前
基础篇六 Nuxt4 状态管理:useState 的正确用法
前端·vue.js·typescript·nuxt4
军军君011 天前
数字孪生监控大屏实战模板:智慧城市大屏
前端·vue.js·typescript·前端框架·echarts·智慧城市·大屏展示
MacroZheng1 天前
全面升级!看看人家的后台管理系统,确实清新优雅!
前端·vue.js·typescript