Когда плохая ссылка доезжает до production

Баги с deep links в vibe-coded приложениях обычно начинаются со скучных input-проблем: malformed user ID, устаревший campaign URL, отсутствующее enum-значение в payload push-уведомления. Дальше приложение считает этот input доверенным, и screen падает глубоко внутри happy-path logic.

Любой URL parameter - это user input. Deep links, payload push-уведомлений, shared URLs, QR-коды - всё это недоверенные данные. Когда вы занимаетесь vibe coding в Cursor, Claude Code прекрасно генерирует happy path. Но paranoid path он не создаёт, если вы явно этого не потребуете. Он делает assumptions. Production наказывает за assumptions.

Валидация Zod на границе маршрута

Проверяйте параметры ровно в той точке, где они попадают в приложение. После валидации остальной код уже работает с типизированными, гарантированно безопасными значениями:

// 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 уже как корректно типизированные значения. Он ничего не знает про useLocalSearchParams. Он не валидирует. Он просто получает хорошие данные. Так и строится vibe-coded приложение, где AI-generated фичи не разваливаются, когда реальный мир присылает мусор.

+native-intent: отсекайте вредоносные URL до входа в приложение

Expo Router даёт вам +native-intent.tsx для обработки входящих native path, которые могут оказаться кривыми, устаревшими или вредоносными. Этот файл выполняется до монтирования ваших providers. Он должен только переписывать пути и отбрасывать плохой input:

// 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 не может проверять, залогинен ли пользователь. Он должен только санитизировать path. Business authorization должна жить в route guards, а не здесь.

Typed routes: ловим ошибки на этапе компиляции

Включите typedRoutes в конфиге и ловите опечатки в названиях маршрутов до того, как они дойдут до пользователей:

// 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 ловят опечатки в именах. Zod validation ловит плохие параметры. Вместе они дают safety net, которого нет у большинства веб-роутеров.

Autotomy Expo Starter Pack уже включает паттерны Zod validation, конфигурацию typed routes и обработчики +native-intent. Ваши deep links работают. Пользователи не падают в crash.