// DaveBuilds

Freelance Marketplace API

A public API, built one slice at a time with an AI agent and checked against real services.

AI-assistedBootcamp project
Live consumer page GitHub
Gigs list with a category filter on the API's consumer page
Consumer page showing seeded sample data

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:

  1. Scaffold, validation, pagination, filters, sorting and response envelopes.
  2. Neon migration and seed.
  3. Rate limiting with Upstash.
  4. Bad-input checks.
  5. README.
  6. Vercel deployment.
  7. 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=5000 is 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:

RequestStatus and error codeWhat it shows
GET /api/v1/gigs?limit=5000200, 100 records, meta.limit 100The limit is clamped to the maximum of 100
GET /api/v1/gigs?offset=-1400 BAD_REQUESTRejects a negative offset
GET /api/v1/gigs?sort=unknown400 BAD_REQUESTRejects an unknown sort field
GET /api/v1/gigs/not-a-valid-id400 BAD_REQUESTRejects a malformed ID
POST /api/v1/orders with only a valid-format gigId422 VALIDATION_ERROR naming clientIdNames 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.

Back to selected work