当坏链接进入 production

Vibe-coded app 里的 deep link bug,通常都始于一些很无聊的 input 问题:malformed user ID、过期的 campaign URL、push payload 里缺失的 enum 值。接着 app 把这些 input 当成可信数据,screen 就会在 happy-path logic 的深处直接 crash。

所有 URL parameter 本质上都是 user input。deep link、push notification payload、shared URL、QR code,它们全都不可信。你用 Cursor 做 Vibe coding 时,Claude Code 会把 happy path 生成得很好,但 paranoid path 不会自己出现,除非你明确要求。它会 assume。production 会惩罚 assumption。

在 Route Boundary 用 Zod Validation

param 应该在刚进入 app 的那一刻就被 validate。过了这一层之后,其余代码只处理 typed 且 guaranteed-safe 的值。

// app/(app)/profile.tsx
// Validate at the gate. Everything past this point is typed.

import { useLocalSearchParams } from 'expo-router'
import { z } from 'zod'

const profileParamsSchema = z.object({
  section: z
    .enum(['overview', 'security', 'preferences'])
    .default('overview'),
  userId: z.string().uuid().optional(),
})

type ProfileParams = z.infer<typeof profileParamsSchema>

export default function ProfileScreen() {
  const raw = useLocalSearchParams()
  const result = profileParamsSchema.safeParse(raw)

  if (!result.success) {
    return <ErrorScreen message='Invalid profile parameters' />
  }

  const { section, userId } = result.data
  return <ProfileContent section={section} userId={userId} />
}

ProfileContent 收到的 sectionuserId 都已经是正确类型、可安全使用的值。它不需要知道 useLocalSearchParams,也不需要自己 validate。它只接收干净数据。这才是构建 Vibe-coded app 的方式:即使真实世界给你喂的是垃圾输入,AI 生成的 feature 也不会被带崩。

+native-intent:在坏 URL 进来之前先拦下它

Expo Router 提供了 +native-intent.tsx,专门处理那些可能 malformed、stale 或 malicious 的 native path。这个 file 会在 provider mount 之前 执行。它应该只做 path rewrite 和坏输入的 reject。

// app/+native-intent.tsx
// This is your firewall, not your business logic.

export function redirectSystemPath({
  path,
}: {
  path: string
}) {
  // Reject paths with suspicious patterns
  if (path.includes('..') || path.includes('//')) {
    return '/'
  }

  // Migrate old URL format to new format
  if (path.startsWith('/user/')) {
    const userId = path.replace('/user/', '')
    if (z.string().uuid().safeParse(userId).success) {
      return `/profile?userId=${userId}`
    }
    return '/'
  }

  return path
}

关键点在这里:+native-intent 没法检查用户是不是已经 logged in。它只应该负责 sanitize path。business authorization 属于 route guard,不属于这里。

Typed Routes:在 compile time 把错误拦住

在 config 里打开 typedRoutes,把 route 名字拼错这种错误挡在用户看到之前。

// app.config.ts
{
  experiments: {
    typedRoutes: true,
  }
}

// Now the router validates at compile time:
router.push('/profile')         // OK
router.push('/profle')          // TypeScript error
router.push({
  pathname: '/profile',
  params: { section: 'overview' },
})  // OK

typed routes 能抓 route 名拼写错误,Zod validation 能抓坏参数。两者配合起来,你会得到一层很多 web router 都没有的 safety net。

Autotomy Expo Starter Pack 已经把 Zod validation pattern、typed route config 和 +native-intent handler 配好了。你的 deep link 能正常工作,用户不会因此 crash。