壊れた link が production に届くとき
Vibe-coded な app の deep link bug は、たいてい退屈な input の問題から始まります。malformed な user ID、古い campaign URL、push payload に欠けた enum 値。そこから app がその input を trusted として扱い、happy-path logic の深いところで screen が crash します。
URL parameter はすべて user input です。deep link、push notification payload、shared URL、QR code。全部 untrusted です。Cursor で Vibe coding していると、Claude Code は happy path を綺麗に生成する。でも paranoid path は、こちらが明示しない限り生成しません。assume する。そして production は assumption を罰します。
Route Boundary で Zod Validation する
param は app に入った瞬間に validate する。そこを越えた先では、残りのコードは typed で 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 が受け取る section と userId は、すでに properly typed な値です。この component は useLocalSearchParams のことを知らない。validate もしない。ただ良い data を受け取るだけ。これが、現実世界から garbage が飛んできても、AI-generated feature が壊れない Vibe-coded app の作り方です。
+native-intent: 入る前に悪い URL を落とす
Expo Router には、malformed、stale、malicious な native path を処理するための +native-intent.tsx があります。この file は provider が mount する前 に動く。ここでやるべきことは path の rewrite と bad input の 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 しているかどうかを確認できません。やるべきなのは path の sanitize だけ。business authorization を置くのは route guard であって、ここではありません。
Typed Routes: compile time でミスを止める
config で typedRoutes を有効にして、route 名の typo をユーザーに届く前に止める。
// 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 名の typo を捕まえる。Zod validation は bad parameter を捕まえる。この 2 つを組み合わせれば、多くの web router にはない safety net が手に入ります。
Autotomy Expo Starter Pack には、Zod validation pattern、typed route config、+native-intent handler が最初から入っています。deep link は動く。ユーザーは crash しません。