Building a React CRUD App with AWS API Gateway
Context: Personal React + AWS API Gateway prototype (private repo). Built to practice CRUD flows, optimistic UI, and serverless deploys. No production users.
AI assist: ChatGPT/Copilot scaffolded some components and Lambda handlers. Prompt logs live in the repo so it’s clear what code I edited.
Status: Demo runs on Vercel (frontend) + AWS API Gateway/Lambda/DynamoDB (backend). Auth, uploads, and Terraform modules are still TODOs.
Reality snapshot
- Frontend: React 18 + Vite, controlled forms, modals, context for auth state, optimistic updates. Hosted on Vercel.
- Backend: AWS API Gateway → Lambda (Node 20) → DynamoDB. API keys + throttling guard the endpoints.
- External data: Jikan API provides trending anime metadata. Cached per session to avoid hitting rate limits.
- Limitations: Auth is mocked, backend cold starts add ~2 s sometimes, and the repo is private until I scrub secrets.
Architecture
ReactJSMobileApp/├── src/│ ├── App.jsx│ ├── api.js│ ├── components/│ └── hooks/useFetch.js├── amplify/ (legacy)├── lambda/│ ├── createCharacter.js│ ├── listCharacters.js│ ├── updateCharacter.js│ └── deleteCharacter.js└── vercel.json
vercel.jsonproxies/api/*requests to API Gateway, injecting the API key server-side so the browser never sees it.- Lambda functions share a thin validation layer and log request IDs for tracing.
Client flow highlights
Fetch & cache characters
useEffect(() => {let active = true;(async () => {setLoading(true);try {const { data } = await api.get("/api/characters");if (active) setCharacters(data);} catch (error) {if (active) setError("Unable to load characters right now.");} finally {if (active) setLoading(false);}})();return () => {active = false;};}, []);
- Guards prevent state updates after unmount. Errors bubble into a toast + log entry.
Optimistic updates
const handleUpdate = async (character) => {const snapshot = characters;setCharacters((prev) =>prev.map((item) => (item.id === character.id ? character : item)));try {await api.put(`/api/characters/${character.id}`, character);} catch {setError("Could not save changes. Reverting.");setCharacters(snapshot);}};
- Keeps the UI responsive even when API Gateway cold starts. Rollback path restores prior state if Lambda fails.
Filtering with memoized selectors
const filteredCharacters = useMemo(() => {if (selectedCategory === "all") return characters;return characters.filter((character) => {const category = character.category?.toLowerCase();const role = character.role?.toLowerCase();return category === selectedCategory || role === selectedCategory;});}, [characters, selectedCategory]);
- Prevents unnecessary renders when the dataset grows.
Deployment + security notes
- Vercel Routes proxy requests to API Gateway, injecting
x-api-key. Rate limits + throttles configured in API Gateway. - Lambda validates payloads with
ajv, normalizes responses, and logs structured JSON. - Terraform/SAM scripts exist but aren’t production-ready—still manual deployments via
sam deploy. - Environment variables live in Vercel secrets + AWS Parameter Store.
Challenges & fixes
- CORS headaches: Solved by proxying through Vercel and enabling CORS on API Gateway.
- Slow cold starts: Mitigated with optimistic UI + skeleton loaders. Future improvement = provisioned concurrency or scheduled warmers.
- Schema drift: Added JSON schema validation + Jest tests so the frontend can’t send malformed payloads.
- Rate limiting: Respect
Retry-Afterfrom Jikan; UI shows a friendly message and caches the last successful response.
TODO list
- Add Cognito auth + user-specific lists.
- Upload images to S3 instead of relying on remote URLs.
- Finish Terraform modules (VPC, API Gateway, DynamoDB) and publish them.
- Write Playwright tests covering the top flows.
- Sanitize the repo so I can make it public without leaking secrets.
How to replay the stack
- Frontend:
npm install && npm run dev(Vite). PointVITE_API_BASE_URLat the deployed gateway. - Backend:
sam build && sam deploy --guidedwith your own stage name. Seeds DynamoDB with sample data fromseed.json. - Smoke test:
npm run test:smoketo create/read/update/delete a character and assert the UI rolls back on failure. - Logs: CloudWatch for Lambda; Vercel for frontend 500s; both include request IDs to correlate.
Failure cases I’ve already hit
- 401/403 from API Gateway: Missing or expired API key; fixed by rotating the key and keeping it server-side only.
- Schema drift: Frontend added a new field; backend rejected it. Added JSON schema validation + contract tests.
- Cold-start UX: Added skeletons and optimistic updates so users aren’t staring at blank pages.
- Caching gone wrong: Stale Jikan responses showed outdated data. Added a “Last refreshed” badge and manual refresh button.
Interview angles
- Trade-offs: Chose serverless to keep costs low and avoid managing servers; acknowledge cold starts and regional latency.
- Security: API key injected at the edge, input validation everywhere, and rate limiting on the public API.
- Honesty: Repo is private until secrets are scrubbed; I’m transparent about what’s missing (auth, uploads, full IaC).
- Testing: Unit + contract tests for the Lambda handlers, planned Playwright for top user journeys.
Stretch goals and open questions
- Should I add offline-first with IndexedDB, or is that overkill for this use case?
- Would AppSync simplify auth + caching compared to raw API Gateway?
- How to balance DX (Vite) with production parity (SAM) without duplicating configs.
- Whether to move to a monorepo (Nx/Turborepo) for shared types between frontend and Lambdas.
Links
- Live demo: https://cruddemo-one.vercel.app/ (backend sleeps when unused; expect a short delay).
- Repo (request access): https://github.com/BradleyMatera/ReactJSMobileApp
- Prompt log + runbook:
docs/folder inside the repo describes every feature, limitation, and AI assist.