Next.js 混合路由踩坑:加了 pages/api 之后的两个隐藏问题
项目用的是 Next.js App Router,但有几个接口需要放在
pages/api下------某些第三方回调必须走 Pages Router,另外想单独暴露几个内部接口供后端调用。官方说两套路由可以共存,实际落地踩了两个坑,网上几乎搜不到答案,记录备查。
环境
- Next.js 16(App Router 主体 +
pages/api混用) - TypeScript 5.x
坑一:加了 pages/api 之后,App Router 的 navigation hook 全变 nullable 了
现象
项目里加了第一个 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'.
涉及所有用到 useParams、usePathname、useSearchParams 的地方。跑起来完全正常,是纯粹的类型误报。
根因
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.json 的 include 覆盖到 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 完整的路由匹配顺序是:
- Headers
beforeFilesrewrites- 静态文件(
public/、_next/static、pages 文件) afterFilesrewritesfallbackrewrites → 最终 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 的混合方案,遇到过其他坑欢迎评论区交流。
参考