Quando um Link Ruim Chega à Produção
Bugs de deep link em apps feitas com vibe coding normalmente começam com problemas de input entediantes: um ID de usuário malformado, uma URL de campanha desatualizada, um valor de enum ausente em um payload de push. Aí o app trata esse input como confiável e uma tela quebra no fundo da lógica de happy path.
Todo parâmetro de URL é input do usuário. Deep links, payloads de push notification, URLs compartilhadas, QR codes: tudo isso é não confiável. Quando você faz vibe coding com Cursor, Claude Code gera o happy path lindamente. Ele não gera o caminho paranoico a menos que você o force. Ele assume. Produção pune suposições.
Validação com Zod na Fronteira da Rota
Valide os params exatamente onde eles entram na app. Depois da validação, o resto do código trabalha com valores tipados e garantidamente 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 recebe section e userId como valores devidamente tipados. Ele não sabe nada sobre useLocalSearchParams. Ele não valida. Só recebe dados bons. É assim que você constrói um app feito com vibe coding em que features geradas por IA não quebram quando o mundo real manda lixo.
+native-intent: Rejeite URLs Maliciosas Antes de Elas Entrarem
O Expo Router oferece +native-intent.tsx para lidar com caminhos nativos recebidos que podem estar malformados, desatualizados ou maliciosos. Esse arquivo roda antes de os providers montarem. Ele deveria apenas reescrever paths e rejeitar entradas ruins:
// 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
}
O insight principal: +native-intent não pode verificar se o usuário está logado. Ele só deveria higienizar paths. Autorização de negócio pertence aos route guards, não aqui.
Typed Routes: Pegue Erros em Compile Time
Ative typedRoutes na config e capture nomes de rota digitados errado antes de eles chegarem aos usuários:
// 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 pegam nomes digitados errado. Validação com Zod pega parâmetros ruins. Entre os dois, você tem uma rede de segurança que a maioria dos roteadores web nem oferece.
O Autotomy Expo Starter Pack inclui padrões de validação com Zod, configuração de typed routes e handlers de +native-intent já prontos. Seus deep links funcionam. Seus usuários não caem em crash.