当 AI 把 Next.js Route 越写越快:我为什么做了 next-route-kit
å½ AI æ Next.js Route è¶åè¶å¿«ï¼æä¸ºä»ä¹åäº next-route-kit
tech_zjf 2026-08-20 0 é 读8åéä¸ä¸ªé¢å Next.js App Router Route Handler çå¯ç»å请æ±åºç¡è®¾æ½ã
GitHubï¼github.com/tech-zjf/neâ¦
npmï¼www.npmjs.com/package/nexâ¦
è¿ä¸¤å¹´ AI å Vibe Coding 让âæä¸ä¸ª Next.js å ¨æ åè½ååºæ¥âè¶æ¥è¶å¿«ã
身边常è§ä¸¤ç§æ¶æï¼ä¸é¨åå¢ééç¨ Monorepoï¼åç«¯ä½¿ç¨ Next.jsãåç«¯ä½¿ç¨ NestJSï¼ä½æ´å¤ä¸å°å¢éãAI 产ååç¬ç«å¼åè ï¼ç´æ¥ä½¿ç¨ Next.js åå ¨æ ââ页é¢ãServer ActionãRoute Handlerãæ°æ®åºè®¿é®é½å¨åä¸ä¸ªé¡¹ç®ã
è¿æ¡è·¯çº¿å¾é«æï¼æèªå·±ä¹å¾å欢ã
é®é¢ä¸å¨ Next.jsï¼èå¨é¡¹ç®è¿å ¥ä¸åæä¹åï¼Route Handler å¾å®¹æåææææ¨ªåé»è¾çæ¶å®¹æã
é´æ â æé â è§£æ Body â æ ¡éª â è°ç¨ Service â try/catch â ç»ä¸ååº â æ¥å¿
å个æ¥å£çèµ·æ¥æ²¡é®é¢ï¼ä½æ¥å£è¶æ¥è¶å¤åï¼Code Review ä¼è¶æ¥è¶çè¦ï¼æ¬æ¥æ³çä¸å¡é»è¾ï¼å´è¦å ç©¿è¿ä¸å±å±éå¤çåºç¡è®¾æ½ä»£ç ã
è¿ä¸æ¯ä¸ä¸ªâ为äºå¼æºèé âçå
æå¼å§ï¼æåªæ¯æ³è®©èªå·±ç Next.js é¡¹ç®æ´å¥½ç»´æ¤ã
æå¯¹å
¶ä¸ä¸ä¸ªå·²ç»ä¸çº¿ç项ç®åäºåªè¯»å®¡æ¥ï¼èå´åªå
å« app/apiï¼
- 430 个 Route Handlerï¼çº¦ 52,783 è¡ï¼
- 203 个 Route ç´æ¥è°ç¨
request.json()ï¼ - 413 个 Route å
å«
try/catchï¼ - 423 个 Route æå·¥æé
NextResponse.json()ï¼ - 357 个 Route èªå·±è·åå½åç¨æ·æè®¤è¯ä¸ä¸æã
è¿äºæ°åä¸ä»£è¡¨â430 个æ¥å£é½åºè¯¥å¥ä¸å±æ½è±¡âãæµå¼ååºãä¸ä¼ ãWebhookã跳转ã夿任å¡ç¼æï¼æ¬æ¥å°±æ´éåä¿çåç Route Handlerã
ä½å®ç¡®å®è¯´æäºä¸ä»¶äºï¼é´æãé误æ å°ãç»ä¸ååºãRequest IDãæ¥å¿ãåæ°è§£æè¿ç±»é»è¾ï¼å·²ç»å¨ Route å±å¤§ééå¤äºã
æä»¥æå å¨èªå·±ç项ç®éï¼æä¸é¨åéå¤åº¦é«ç JSON API æè¿ä¸ªæè·¯éæäºä¸éã
ç»è¿å®é 使ç¨åï¼æ¥å£è¡ä¸ºæ´ç¨³å®äºï¼æ´ç´æ¥çæåæ¯ï¼Route æä»¶çé 读路å¾åæ¸ æ¥äºä¸å°ï¼ä»¥åè¦å ç认è¯ãå¼å¸¸åååºæ¨¡æ¿ï¼ç°å¨å¯ä»¥æ´å¿«å®ä½âè¿ä¸ªæ¥å£ç©¶ç«åä»ä¹ä¸å¡âã
äºæ¯ææè¿é¨åéç¨è½åæ½åºæ¥ï¼next-route-kitã
宿ªå¿ é忿项ç®ï¼ä½å¦æä½ ç Next.js 项ç®ä¹æç±»ä¼¼é®é¢ï¼å¸æå®è½è§£å³å ¶ä¸ä¸é¨åã
ä¸ä¸ªçå®ä¸å¸¸è§ç Route é¿ä»ä¹æ ·
ä¸é¢æ¯ä»çå®é¡¹ç®æ½åºçç»æï¼ææä¸å¡åãæ¥å£ååå é¨å®ç°ç»èåå·²è±æï¼
// app/api/workspaces/[workspaceId]/records/route.ts
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ workspaceId: string }> },
) {
try {
// 1. é´æ
const auth = await getCurrentAuth(request)
if (!auth) {
return NextResponse.json(API_RESPONSE.UNAUTHORIZED, {
status: 401,
})
}
// 2. è·¯ç±åæ°
const { workspaceId } = await params
// 3. æé
const canWrite = await WorkspaceService.canWrite(
auth.userId,
workspaceId,
)
if (!canWrite) {
return NextResponse.json(API_RESPONSE.FORBIDDEN, {
status: 403,
})
}
// 4. è§£æåæ ¡éª
const raw = (await request.json()) as Partial<CreateRecordInput>
const input = parseCreateRecord(raw)
// 5. ä¸å¡
const record = await RecordService.create({
workspaceId,
operatorId: auth.userId,
...input,
})
// 6. æåååº
return NextResponse.json({
...API_RESPONSE.SUCCESS,
data: { record },
})
} catch (error) {
console.error('Create record failed:', error)
// 7. å¼å¸¸ååº
return NextResponse.json(API_RESPONSE.INTERNAL_SERVER_ERROR, {
status: 500,
})
}
}
è¿æ®µä»£ç æ¬èº«æ²¡æéï¼æ¥å£ä¸å¤æ¶ä¹å¾ç´è§ã
é®é¢å¨äºï¼éçæ¥å£å¢å ï¼è®¤è¯ãæåæéãJSON è§£æãåæ°æ ¡éªãååºç»æåå¼å¸¸å¤çä¼å¤å¶å°æ¯ä¸ªæä»¶ãä¸åå¼åè åç¨å¾®ååºä¸å飿 ¼ï¼
{ code: 0, msg: 'success', data: {} }
{ code: 'OK', message: 'success', data: [] }
{ error: 'Forbidden' }
å端éåä¸å¾ä¸åæ¶å¤æ HTTP Statusãä¸å¡ç åä¸ç¨³å®çæ°æ®ç»æï¼åä¸ä¸ªé误å¯è½å¼¹å ¨å± Toastï¼ä¹å¯è½è¢«é¡µé¢å±é¨åå¤ç䏿¬¡ã
çæ£é¾ç»´æ¤ç䏿¯æä¸è¡ä»£ç ï¼èæ¯æ¥å£å¥çº¦åé误çç¥å¼å§ååã
社åºå·²ç»ææ¹æ¡ï¼ä¸ºä»ä¹è¿è¦åä¸ä¸ªï¼
社åºå¹¶ä¸ç¼ºå·¥å ·ï¼åªæ¯å®ä»¬è§£å³çæ¯ä¸åå±çé®é¢ã
- next-safe-action å¾éå Server Actionsï¼å®æä¾ middlewareãè¾å ¥æ ¡éªå客æ·ç«¯è°ç¨é¾è·¯ã
- Hono æ¯æçç Web æ åè·¯ç±æ¡æ¶ï¼å¯ä»¥æè½½å° Next.js ç catch-all Route ä¸ã
- next-connect æä¾ Next.js çæ¹æ³è·¯ç±ä¸ middleware ç»åã
å®ä»¬é½ä¸æ¯âæé®é¢âï¼åªæ¯åæå½æ¶çéæ±è¾¹çä¸å®å ¨ä¸è´ã
æçè¯æ±æ´çªï¼
䏿¿æ¢ Next.js çæä»¶è·¯ç±ï¼ä¿çåç
Request/Responseï¼åªæéå¤ç请æ±çº§çç¥æ½å°æ¾å¼ãä¸å¯åãå¯ç»åç Factory ä½ç¨åä¸ã
è¿ä¹æ¯ next-route-kit çè¾¹çã
éæåï¼ä¸å¡æ¥å£åªä¿çä¸å¡é 读路å¾
å 卿®éæå¡ç«¯æ¨¡åä¸å®ä¹å ±äº«çç¥ï¼
// src/server/routes.ts
import {
ApiException,
apiResponsePlugin,
createRoute,
unauthorized,
type AnyRouteContext,
type Guard,
type RouteMiddleware,
} from 'next-route-kit'
// ä¸å¡é¡¹ç®èªå·±ç»´æ¤ä¸å¡ç ï¼å
䏿¿ä½ å®ä¹è¡ä¸è¯ä¹ã
export const ApiCode = {
OK: { code: 'OK', msg: 'success' },
UNAUTHORIZED: {
code: 'UNAUTHORIZED',
msg: 'Sign in required',
status: 401,
},
FORBIDDEN: {
code: 'FORBIDDEN',
msg: 'Permission denied',
status: 403,
},
INTERNAL_ERROR: {
code: 'INTERNAL_ERROR',
msg: 'Internal server error',
},
} as const
type AppLocals = {
requestId: string
userId?: string
}
type AppContext = AnyRouteContext<AppLocals>
const requestContext: RouteMiddleware<AppContext> = {
name: 'request-context',
use(context, next) {
context.locals.requestId =
context.request.headers.get('x-request-id') ?? crypto.randomUUID()
return next()
},
}
const requireUser: Guard<AppContext> = {
name: 'require-user',
async canActivate(context) {
// æ¿æ¢ä¸ºé¡¹ç®èªå·±ç认è¯å®ç°ã
const session = await getSessionFromRequest(context.request)
if (!session) {
throw unauthorized()
}
context.locals.userId = session.userId
return true
},
}
// ææä» apiRoute æ´¾çç Route é½ç»§æ¿è¿äºçç¥ã
export const apiRoute = createRoute<AppLocals>({
middleware: [requestContext],
plugins: [
apiResponsePlugin({
success: ApiCode.OK,
systemError: ApiCode.INTERNAL_ERROR,
}),
],
})
// è®¤è¯ Route æ¯ apiRoute çä¸å¯ååä½ç¨åã
export const authenticatedRoute = apiRoute.extend({
guards: [requireUser],
})
ç¶åä¸å¡æ¥å£åæï¼
// app/api/workspaces/[workspaceId]/records/route.ts
import { jsonBody } from 'next-route-kit'
import { authenticatedRoute } from '@/src/server/routes'
type RouteParams = { workspaceId: string }
type CreateRecordInput = { title: string; content: string }
export const POST = authenticatedRoute<RouteParams, CreateRecordInput>({
// åªæç¡®å®éè¦æ¶æå£°æèªå¨ JSON è§£æã
body: jsonBody<CreateRecordInput>(),
handler: async (_request, { params, body, locals }) => {
const canWrite = await WorkspaceService.canWrite(
locals.userId!,
params.workspaceId,
)
if (!canWrite) {
throw new ApiException(ApiCode.FORBIDDEN)
}
const record = await RecordService.create({
workspaceId: params.workspaceId,
operatorId: locals.userId!,
...body,
})
return { record }
},
})
ç°å¨ Code Review çé 读路å¾åæï¼
è¿ä¸ªæ¥å£éè¦ç»å½
â 读å workspaceId å body
â 夿æ¯å¦æåæé
â å建记å½
â è¿åç»æ
æ§å¶æµæ²¡ææ¶å¤±ï¼åªæ¯æâæææ¥å£é½ä¸æ ·çé¨åâæ¾å°äºä¸ä¸ªå¯è§ã坿µè¯çå ±äº«ä½ç½®ã
为ä»ä¹ Handler ä»ç¶ä¿çåç Request
æä¸å¸æä¸ºäºâä¼é âèåé æ°ç认ç¥è´æ ã
æä»¥ Handler ç第ä¸ä¸ªåæ°å§ç»æ¯åç Web Requestï¼
export const GET = authenticatedRoute({
handler: async (request, { locals }) => {
const url = new URL(request.url)
return RecordService.list({
operatorId: locals.userId!,
page: Number(url.searchParams.get('page') ?? 1),
})
},
})
paramsï¼Next.js å¨æè·¯ç±åæ°ï¼bodyï¼ä» å¨å£°æjsonBody()åæä¾ï¼queryï¼ä» å¨å£°æquery()åæä¾ï¼localsï¼Middleware / Guard 为å½å请æ±åå ¥çå ±äº«æ°æ®ï¼requestï¼ä»ç¶æ¯ä½ çæçåç请æ±å¯¹è±¡ã
没æå¼ºå¶ç args 大对象ï¼ä¹æ²¡æè¯ä¹æ¨¡ç³ç stateã
ç»ä¸ååºä¸æ¯âç»ä¸ HTTP ç¶æç â
å¾å¤é¡¹ç®ä¼æ··æ·ä¸¤ä»¶äºï¼
- HTTP Statusï¼æè¿°åè®®å±ç»æï¼å¦ 401ã403ã409ã500ï¼
- ä¸å¡ç ï¼æè¿°å端稳å®å¯åæ¯çä¸å¡è¯ä¹ï¼å¦
QUOTA_EXCEEDEDãPLAN_REQUIREDã
next-route-kit ç apiResponsePlugin() æ¯å¯éæä»¶ãå¯ç¨åï¼æ®é对象åä¸å¡å¼å¸¸é½ä¼å¨ Route è¾¹çæ¶æä¸ºï¼
{
code: 'OK',
msg: 'success',
data: {
// æ°¸è¿æ¯å¯¹è±¡ï¼æ¹ä¾¿åç»æ©å±
},
}
ä¸å¡å±åªéæåºç±»ååå¼å¸¸ï¼
if (remainingQuota < 1) {
throw new ApiException(ApiCode.QUOTA_EXCEEDED, {
data: { remainingQuota },
})
}
HTTP Status ä»ç¶ä¿çï¼ä¾å¦é é¢å²çªå¯ä»¥æ¯ 409ï¼ä½å端ä¸éè¦åé å符串 message çä¸å¡ç¶æï¼
if (payload.code === ApiCode.QUOTA_EXCEEDED.code) {
openUpgradeDialog()
}
å ä¸ä¼æ¿åºç¨å³å® Toastãå¼¹çªè¿æ¯é¡µé¢é误æââ飿¯äº§åå±çèè´£ãå®è§£å³çæ¯è®©å端æ¿å°ç¨³å®ä¸ç»ä¸çæ¥å£å¥çº¦ã
请æ±é¾è·¯ä¸ NestJS çå ³ç³»
è¿ä¸ªé¡¹ç®åé´çæ¯ NestJS ä¸â横åå ³æ³¨ç¹ææç¡®ä½ç½®âçæè·¯ï¼ä½ä¸ç §æ¬ ControllerãDecoratorãModule å DI 容å¨ã
å®é 请æ±é¡ºåºæ¯ï¼
Next params hydration
â Middleware
â Guard
â Interceptor enter
â 声æç Body / Query è§£æ
â Pipe
â Handler(request, context)
â Interceptor exit
â Response Serializer
Exception Filter è¦çæ´ä¸ªé¾è·¯ã
ä¸ä¸ªéè¦ç»èï¼Guard å¨ JSON Body è§£æä¹åæ§è¡ãæªç»å½ææ æéç请æ±ï¼ä¸ä¼å æ¶è´¹åªè½è¯»å䏿¬¡ç Request Bodyã
ææè½åé½å¯ä»¥æèå´æ³¨å ¥
é¤äºå ç½®ååºæä»¶ï¼ä¹å¯ä»¥èªå®ä¹æä»¶ï¼æå¤ä¸ªæ¨ªåçç¥ä½ä¸ºä¸ä¸ªå¯å¤ç¨åå æ³¨å ¥ï¼
import { type RoutePlugin } from 'next-route-kit'
class RequestTimingPlugin implements RoutePlugin {
readonly name = 'request-timing'
readonly runtime = 'both' as const
install() {
return {
interceptors: [
{
name: 'request-timing',
async intercept(context, next) {
const startedAt = Date.now()
try {
return await next()
} finally {
console.info({
requestId: context.locals.requestId,
pathname: context.meta.pathname,
durationMs: Date.now() - startedAt,
})
}
},
},
],
}
}
}
坿³¨å ¥çä½ç½®æä¸å±ï¼
createRoute({ plugins }) æææ´¾ç Route
â route.extend({ plugins }) æä¸ªä¸å¡è¾¹ç
â route({ use: [plugin] }) å个æ¥å£
常è§ç¨éå æ¬ Request IDã审计æ¥å¿ãæéãç¼åãè¶ æ¶ãå¼å¸¸æ å°ãç»ä¸ååºåå¯è§æµæ§ã
宿´çæä»¶å¥çº¦ä¸æ§è¡é¡ºåºè§ï¼æä»¶æåã
ä»ä¹æ¶åä¸è¯¥ä½¿ç¨å®ï¼
ä¸è¦ä¸ºäºç»ä¸èç»ä¸ã
以ä¸åºæ¯é常继ç»ä½¿ç¨åç Next.js Route Handler æ´æ¸ æ°ï¼
- æµå¼ååºï¼
- æä»¶ä¸ä¼ ä¸ Multipartï¼
- ç¾åæ ¡éªç Webhookï¼
- éå®åï¼
- æå ¶ç®åç䏿¬¡æ§æ¥å£ï¼
- é度ä¾èµæä¸ªåè®®æç¬¬ä¸æ¹ SDK çè¾¹çæ¥å£ã
宿éåçæ¯ï¼é¡¹ç®ä¸åå¨å¤§é JSON APIï¼ä¸è®¤è¯ãæéãè¾å ¥æ ¡éªãå¼å¸¸æ å°ãååºæ ¼å¼ç横åé»è¾å·²ç»ææ¾éå¤ã
å¼å§ä½¿ç¨
npm install next-route-kit
# åªæä½¿ç¨ Zod æ¶æå®è£
ï¼ä¸»å
ä¸ä¾èµ Zodã
npm install @next-route-kit/zod zod
- GitHubï¼github.com/tech-zjf/neâ¦
- npmï¼www.npmjs.com/package/nexâ¦
- ä¸æææ¡£ï¼github.com/tech-zjf/neâ¦
- API ååºä¸ä¸å¡ç ï¼github.com/tech-zjf/neâ¦
Next.js çæä»¶è·¯ç±ååç Request / Response é½ä¿çå¨åä½ï¼å åªè´è´£è¯·æ±ç®¡éä¸å¯ææçç¥ã
å¦æä½ ç项ç®é乿âä¸ä¸ª Route æä»¶ 70% 齿¯é´æãtry/catch å NextResponse.jsonâçæè§ï¼æ¬¢è¿äº¤æµçå®è¿ç§»æ¡ä¾ãå½åæè§å使ç¨åé¦ã
Aitishiku.com