DEV Community

Cover image for I Ship Paid APIs in a Weekend. Here's the Exact Stack I Use Every Time.
Mr Hamlin
Mr Hamlin

Posted on

I Ship Paid APIs in a Weekend. Here's the Exact Stack I Use Every Time.

I've shipped three paid API products in the past six months. Each one took a weekend to reach a working, billable state — users signing up, generating API keys, making authenticated requests, and paying me through Stripe.

Not because I'm fast. Because the infrastructure is the same every time.

Auth is the same. API key management is the same. Stripe billing is the same. Rate limiting, usage tracking, the dashboard — all the same. The only thing that changes is the business logic behind the endpoints.

After the third time wiring Stripe webhooks to Supabase to API key validation, I wrote down the pattern. Here it is.


The Stack

Layer Tool Cost
Runtime Node.js + Express Free
Auth & Database Supabase Free tier
Billing Stripe 2.9% + 30¢ per txn
Rate Limiting Redis (Upstash) Free tier
Dashboard React + Vite Free
Deployment Railway $5/month

Total cost at launch: $5/month. Everything else is free until you have paying customers.

Why these specific tools? I've tried the alternatives. Supabase beats Firebase for API businesses because relational data (users → keys → usage logs) is natural in Postgres and painful in document stores. Express beats Next.js for API servers because you don't need SSR, file-based routing, or React Server Components — you need a fast request-response pipeline. Upstash Redis beats Postgres for rate limiting because checking a counter needs to be sub-millisecond, not 5-20ms.

The Middleware Chain

This is the architecture pattern that makes everything work. Every API request passes through a chain of middleware before it hits your business logic:

Request arrives
  → validateApiKey    (reject if invalid)
  → rateLimit         (reject if over per-minute limit)
  → trackUsage        (reject if monthly quota exceeded)
  → YOUR LOGIC        (do the actual work)
Response sent
Enter fullscreen mode Exit fullscreen mode

By the time your route handler runs, you know three things: the key is valid, the request is within rate limits, and the monthly quota hasn't been exceeded. Your business logic never checks any of this. The middleware guarantees it.

API Keys: The Pattern Everyone Skips

Most tutorials use Bearer tokens for API auth and move on. That doesn't work for a business. Your customers need to generate keys from a dashboard, name them, revoke them, and see when they were last used.

The security model matters:

  • Never store raw keys. Store a SHA-256 hash. When a request comes in, hash the provided key and look up the hash.
  • Show the key once. After generation, you only show a masked preview like sk_live_abc...xyz. Lost key? Generate a new one.
  • Use a recognizable prefix. sk_live_ tells GitHub's secret scanning that this is a credential. A leaked prefixed key gets caught before it causes damage.
import crypto from 'crypto';

const PREFIX = 'sk_live_';

function generateApiKey() {
  const random = crypto.randomBytes(32).toString('hex');
  const raw = `${PREFIX}${random}`;
  const hash = crypto.createHash('sha256')
    .update(raw).digest('hex');
  return { raw, hash };
}
Enter fullscreen mode Exit fullscreen mode

256 bits of entropy. The same pattern Stripe and OpenAI use.

Rate Limiting: The Sliding Window

A fixed window rate limiter resets at the top of every minute, which means a customer can send their full quota at 0:59 and again at 1:01 — effectively doubling their rate. The sliding window counts the actual last 60 seconds from right now.

Four Redis commands, one pipeline, one network round-trip:

async function checkRateLimit(apiKeyId, maxPerMinute) {
  const key = `ratelimit:${apiKeyId}`;
  const now = Date.now();
  const windowStart = now - 60000;

  const pipe = redis.pipeline();
  pipe.zremrangebyscore(key, 0, windowStart); // remove old
  pipe.zadd(key, now, `${now}`);              // add current
  pipe.zcard(key);                            // count window
  pipe.expire(key, 60);                       // auto-cleanup

  const results = await pipe.exec();
  const count = results[2][1];
  return {
    allowed: count <= maxPerMinute,
    remaining: Math.max(0, maxPerMinute - count),
  };
}
Enter fullscreen mode Exit fullscreen mode

Then set the standard headers on every response — not just 429s:

  • X-RateLimit-Limit — max per minute for this tier
  • X-RateLimit-Remaining — requests left in window
  • X-RateLimit-Reset — when the window resets

Your API consumers use these headers to implement client-side backoff. The good ones slow down before they hit the wall.

Stripe Billing: The Four Webhooks That Matter

Stripe sends dozens of event types. You need four:

  1. checkout.session.completed — customer paid. Create the subscription record, upgrade their tier, update all their API keys to the new limits.

  2. customer.subscription.updated — plan change or renewal. Look up the new price, determine the tier, update the database.

  3. customer.subscription.deleted — cancellation. Downgrade to free tier.

  4. invoice.payment_failed — card declined. Flag the account, notify the user.

The critical gotcha: Stripe signs webhooks against the raw request body. If Express parses the body into JSON first, the signature verification fails. Mount your webhook route with express.raw() before express.json():

// This line MUST come before express.json()
app.use('/billing/webhook',
  express.raw({ type: 'application/json' }));
app.use(express.json());
Enter fullscreen mode Exit fullscreen mode

I lost two hours to this bug on my first API. You don't have to.

The Weekend Timeline

Here's how I actually spend the weekend:

Friday evening: Set up the project, install deps, create Supabase project, get a health check endpoint running. 1 hour.

Saturday morning: Auth routes, API key system, validation middleware, first authenticated API call. 3 hours.

Saturday afternoon: Rate limiting in Redis, Stripe billing with all four webhook handlers. 3 hours.

Sunday morning: Usage tracking, React dashboard with five pages (login, overview, keys, usage, billing). 2 hours.

Sunday afternoon: Deploy to Railway, custom domain, write docs, post about it. 2 hours.

Nine hours of building. A live, paid API on the internet.

What I Learned

Price higher than you think. At $9/month you need 111 customers to make $1,000. At $29/month you need 35. At $79/month you need 13. Fewer customers = less support = more time building.

The free tier is a conversion mechanism. A developer integrates your API, their project succeeds, they hit the limit, they upgrade. By then they've written code against your endpoints. Switching costs are real.

Good docs beat good marketing. A developer who can integrate in five minutes becomes a customer. A developer who has to email you a question probably doesn't.


I wrote the full process — all twelve chapters, every code file, the complete middleware chain, the dashboard, deployment, and launch strategy — into a book:

The Solo Developer's Guide to Shipping a Paid API in a Weekend — 82 pages, $29, every line of code is production code.

If you've been sitting on an API idea waiting for the right time to build the billing infrastructure, the right time is this weekend.


I'm Brent Hamlin. I build paid APIs and write about the stack behind them. Find me at dev.to/mr_hamlin.

Top comments (0)