Los bugs de deep links en apps hechas con vibe coding suelen empezar con problemas de input aburridos: un ID de usuario malformado, una URL de campaña desactualizada, un valor de enum que falta en un payload de push. Luego la app trata ese input como si fuera confiable y una pantalla hace crash en lo más profundo de la lógica de happy path.

Todo parámetro de URL es input del usuario. Deep links, payloads de push notification, URLs compartidas, códigos QR: todo es no confiable. Cuando haces vibe coding con Cursor, Claude Code genera el happy path de maravilla. No genera el camino paranoico a menos que lo obligues. Asume. Producción castiga las suposiciones.

Validación con Zod en el boundary de la ruta

Valida los params justo donde entran a la app. Después de validarlos, el resto de tu código trabaja con valores tipados y garantizados como seguros:

// 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 recibe section y userId como valores correctamente tipados. No sabe nada de useLocalSearchParams. No valida. Solo recibe datos buenos. Así construyes una app hecha con vibe coding en la que las features generadas por IA no se rompen cuando el mundo real les envía basura.

+native-intent: rechaza URLs maliciosas antes de que entren

Expo Router te da +native-intent.tsx para manejar rutas nativas entrantes que podrían estar malformadas, obsoletas o ser maliciosas. Este archivo corre antes de que se monten tus providers. Solo debería reescribir rutas y rechazar input inválido:

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

La idea clave: +native-intent no puede comprobar si el usuario está autenticado. Solo debería sanear rutas. La autorización de negocio pertenece a los route guards, no aquí.

Typed Routes: detecta errores en tiempo de compilación

Activa typedRoutes en tu config y detecta nombres de ruta mal escritos antes de que lleguen a los usuarios:

// 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 detectan nombres mal escritos. La validación con Zod atrapa params inválidos. Entre ambas cosas, tienes una red de seguridad que la mayoría de los routers web no ofrecen.

El Autotomy Expo Starter Pack incluye patrones de validación con Zod, configuración de typed routes y handlers de +native-intent preconfigurados. Tus deep links funcionan. Tus usuarios no sufren crashes.