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 的那一刻就被驗證。過了這一層之後,剩下的程式碼就只處理有型別、而且保證安全的值:

// 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。它不負責 validation。它只負責接收乾淨資料。這才是你做 Vibe-coded app 時,能讓 AI 生成的新功能在現實世界丟來垃圾輸入時也不會炸掉的方式。

+native-intent:在它進入 app 前先擋掉惡意 URL

Expo Router 提供了 +native-intent.tsx,專門處理那些可能 malformed、過期,甚至帶惡意的 native path。這個檔案會在 providers 掛載之前 就先執行。它只應該負責改寫 path,和拒絕壞資料:

// 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
}

關鍵 insight 是:+native-intent 不能檢查使用者有沒有登入。它只應該負責清洗 path。business authorization 應該放在 route guard,不是放在這裡。

Typed Routes:在編譯期抓到錯誤

在 config 裡開啟 typedRoutes,你就能在 route name 打錯時,還沒到使用者手上之前就先被攔下來:

// 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 name。Zod validation 會抓壞掉的參數。兩者一起用,你就得到一張大多數 web router 根本沒有的安全網。

Autotomy Expo Starter Pack 已經把 Zod validation pattern、typed route 設定,以及 +native-intent handler 都先配好了。你的 deep link 可以正常工作,使用者也不會因為點了一個 link 就 crash。