Decision Record: Designing a Reliable Contact Pipeline
How I designed contact submission handling with abuse controls, predictable failure behavior, and minimal operational overhead.
Context
The portfolio contact form looked simple at the UI layer, but production behavior had to cover several non-trivial constraints:
- prevent abuse without degrading real submissions
- avoid silent data loss between transport and persistence
- keep operational complexity low for a solo-maintained project
I wanted a design that was stable under low-to-moderate traffic and still clear enough to reason about during failures.
Decision
I used a Server Action-based pipeline with this order:
- validate inputs with Zod
- apply honeypot and server-side rate limiting
- send notification email
- persist to database when configured
- return explicit success/error state to the UI
This keeps mutation logic server-side while avoiding custom API route complexity.
Tradeoffs Considered
Option A: API Route + client fetch
Pros:
- familiar REST shape
- easy to test independently
Cons:
- more boilerplate for this scope
- more client/server wiring for state and error handling
Option B: Server Actions (chosen)
Pros:
- less transport boilerplate
- direct form integration with React state flow
- cleaner mutation ownership in one place
Cons:
- fewer teams are deeply familiar with this pattern
- requires careful handling of returned action state
Failure Modes Addressed
- Spam flood: mitigated with honeypot + Upstash-backed server-side rate limiting
- Validation drift: single server schema controls accepted shape
- Partial delivery risk: success criteria tied to server-side completion path, not optimistic UI
- Operational mismatch across environments: env-driven configuration with explicit fallback behavior
Outcome Signal (Technical)
The current implementation provides deterministic server-side validation, durable abuse controls, and explicit action-state feedback without introducing additional API infrastructure. It also supports optional persistence while maintaining predictable behavior when DB configuration is absent.
What I Would Improve Next
- add structured error categories for observability
- record lightweight submission telemetry for anomaly analysis
- add queue-backed async delivery path if traffic profile changes