Problem
This was Task 1 of the Product Engineering Bootcamp: build a public API, deploy it and prove it works from outside its own codebase. I wanted to practise versioned paths, pagination, useful error codes, rate limiting and documentation someone else could follow.
The API exposes five resources — freelancers, clients, gigs, orders and reviews — plus order creation. The consumer page lists gigs with a category filter. Its contents are seeded sample data.
My role
I chose the marketplace and how the five resources connect. I drafted the brief, AGENTS.md, Prisma schema and seed script with Claude, then directed Antigravity to build the API one slice at a time.
I set up Neon, Upstash and Vercel, tested each slice against the real services, and debugged the live deployment. This was AI-assisted development with my direction, review and testing.
Process
The stack was Next.js 14 (App Router), strict TypeScript, Prisma, PostgreSQL on Neon, Zod, Upstash Redis and Vercel.
I gave the agent these slices, in order:
- Scaffold, validation, pagination, filters, sorting and response envelopes.
- Neon migration and seed.
- Rate limiting with Upstash.
- Bad-input checks.
- README.
- Vercel deployment.
- Consumer page.
The seed contained 150 freelancers, 200 clients, 372 gigs, 400 orders and 65 reviews. These were sample records, not users. Running the seed again skipped existing data.
I checked unit tests and real responses from Neon and Upstash. Both database URLs initially pointed at the pooled Neon endpoint, so I changed DIRECT_URL to the unpooled endpoint.
Not every suggestion I made was right. I proposed timeout: 0 for the rate limiter; the agent checked the library source and showed why that change was wrong, so I left it alone.
The Upstash URL first had stray whitespace, then was invalid, causing the limiter to fail closed with 503 responses. My network also rotated IP addresses between requests, which made the limiter look broken. Testing from a stable connection separated the network issue from the API behavior.
Key decisions
- cuid IDs. Non-sequential IDs avoid the simple counting pattern of sequential IDs; they do not replace authorization.
- Offset pagination with a limit of 100. A request for
limit=5000is clamped to 100. It keeps responses bounded, but changing data can cause skipped or repeated rows between pages. - Consistent response shapes. Success uses
{ data, meta }; failures use{ error: { code, message } }with an appropriate HTTP status. - 100 requests per minute per IP, failing closed. A Redis outage produces 503 responses instead of leaving the API unmetered. The tradeoff is that a Redis outage also makes the API unavailable.
- Snapshot order prices. Changing a freelancer's price later must not change the price recorded for an existing order.
Result
The API and consumer page were deployed to Vercel. My project notes record seven passing tests on main and a stable-connection check where 100 requests succeeded, followed by 429 responses with a Retry-After header and the standard error body.
These are recorded project checks, not a fresh test run performed for this portfolio. The notes also record these input checks:
| Request | Status and error code | What it shows |
|---|---|---|
GET /api/v1/gigs?limit=5000 | 200, 100 records, meta.limit 100 | The limit is clamped to the maximum of 100 |
GET /api/v1/gigs?offset=-1 | 400 BAD_REQUEST | Rejects a negative offset |
GET /api/v1/gigs?sort=unknown | 400 BAD_REQUEST | Rejects an unknown sort field |
GET /api/v1/gigs/not-a-valid-id | 400 BAD_REQUEST | Rejects a malformed ID |
POST /api/v1/orders with only a valid-format gigId | 422 VALIDATION_ERROR naming clientId | Names the missing required field |
What I'd do differently
Authentication on write endpoints comes first: at the point documented in my notes, anyone could create an order for any clientId. An idempotency key comes next, because repeating a valid request creates another order.
Per-IP rate limiting can be bypassed by rotating IPs, and offset pagination can skip or duplicate rows when data changes. The documented version also lacks CORS headers, CI and a license.
I would bring the idempotency approach from Order Confirmation Jobs into the order-creation endpoint after adding authentication.