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.json proxies /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-After from 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). Point VITE_API_BASE_URL at the deployed gateway.
  • Backend: sam build && sam deploy --guided with your own stage name. Seeds DynamoDB with sample data from seed.json.
  • Smoke test: npm run test:smoke to 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