WEB API's Lesson 35 – Real-World Use Cases | Dataplexa
Web APIs · Lesson 35

Real-World Use Cases

Map the API patterns you have built to five production scenarios — payments, notifications, search, file uploads, and third-party integrations — and see how the same fundamentals apply across completely different domains.

Every API course eventually hits a wall between theory and practice. You understand REST, you know what a 401 means, you have built a gateway and written auth middleware — but when a product manager asks you to "add Stripe payments" or "connect to Slack notifications," the gap between what you know and what you need to do feels wider than it should.

That gap is not a knowledge gap. It is a pattern-matching gap. Every real-world integration, regardless of the third-party service involved, uses the same handful of patterns you have already studied. A payment flow is auth plus idempotency plus webhooks. A notification system is event-driven webhooks plus rate limiting. A file upload is multipart requests plus pre-signed URLs. Once you see the pattern, the specific service is just a different set of endpoint names.

This lesson works through five use cases the APIForge Product team is building right now. For each one: the problem, the API design decision, the request and response shape, and the exact pattern from earlier lessons that it applies.

Use Case 1 — Payments
Charge a customer safely using idempotency keys and handle payment failures with structured errors. Patterns: idempotency, error objects, webhooks.
Use Case 2 — Notifications
Send Slack alerts when APIForge events happen without blocking the API response. Patterns: async jobs, rate limiting, webhook verification.
Use Case 3 — Search
Add full-text project search that stays fast as the dataset grows. Patterns: query parameters, cursor pagination, caching.
Use Case 4 — File Uploads
Let users attach files to projects without routing large payloads through the API server. Patterns: pre-signed URLs, multipart requests, async processing.
Use Case 5 — Third-Party Integrations
Connect APIForge to GitHub so project activity triggers API calls automatically. Patterns: OAuth tokens, webhook ingestion, API key rotation.

Use Case 1 — Processing Payments Safely

The APIForge Product team is adding a billing feature. When a team upgrades to the Pro plan, the API needs to charge their card, handle declines gracefully, and confirm the subscription activated. Three things can go wrong: the card can be declined, the network can drop mid-request, or the webhook confirming payment can arrive out of order.

The pattern: the APIForge backend never stores card details — it passes a payment token from the frontend to Stripe and receives a charge ID back. The idempotency key on the request ensures that if the network drops and the frontend retries, the charge runs exactly once. Stripe then sends a webhook confirming the payment outcome, which is how the subscription gets activated — not synchronously in the charge response.

Why activate via webhook, not the charge response?
The charge API response tells you the payment was submitted — not that the money settled. Some payment methods (bank transfers, certain cards) take hours or days to confirm. Activating the subscription on the charge response means you might activate a subscription that ultimately fails. Activating on the payment_intent.succeeded webhook means you activate only when the money is confirmed.
// WHAT: APIForge billing — charge a customer and handle all failure cases
// File: src/controllers/billing.controller.js
// Uses Stripe idempotency key to safely handle network retries

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export async function createSubscription(req, res) {
  const { paymentMethodId, planId } = req.body;
  const userId = req.user.userId;

  // Idempotency key scoped to user + plan — retries are safe
  const idempotencyKey = `sub_${userId}_${planId}_${Date.now()}`;

  try {
    // Step 1: Create or retrieve Stripe customer
    const customer = await stripe.customers.create(
      { email: req.user.email, payment_method: paymentMethodId },
      { idempotencyKey: `cus_${userId}` }
    );

    // Step 2: Create the subscription — activation happens via webhook
    const subscription = await stripe.subscriptions.create({
      customer:          customer.id,
      items:             [{ price: planId }],
      payment_behavior:  'default_incomplete',
      expand:            ['latest_invoice.payment_intent'],
    }, { idempotencyKey });

    const paymentIntent = subscription.latest_invoice.payment_intent;

    // Return the client secret — frontend uses this to confirm the payment
    res.status(202).json({
      subscriptionId:      subscription.id,
      clientSecret:        paymentIntent.client_secret,
      status:              subscription.status,  // 'incomplete' until webhook fires
    });

  } catch (err) {
    // Stripe error codes map directly to user-facing messages
    const messages = {
      card_declined:          'Your card was declined. Please try a different card.',
      insufficient_funds:     'Your card has insufficient funds.',
      card_velocity_exceeded: 'Too many attempts. Please wait before trying again.',
    };

    res.status(402).json({
      error: {
        type:    'payment_error',
        code:    err.code,
        message: messages[err.code] || 'Payment could not be processed.',
        param:   'paymentMethodId',
      }
    });
  }
}
POST /api/billing/subscribe Content-Type: application/json Idempotency-Key: sub_usr_9f3k2m_price_pro_1731580800 { "paymentMethodId": "pm_1OqLk2LkdIwHu7ixVMkqcSQS", "planId": "price_pro_monthly" } HTTP/1.1 202 Accepted Content-Type: application/json { "subscriptionId": "sub_1OqLk3LkdIwHu7ix4F8rKpQM", "clientSecret": "pi_3OqLk2LkdIwHu7ix1_secret_vKp2mNqRtLwY3nZxBcD", "status": "incomplete" } → Frontend uses clientSecret to confirm payment in the browser → Stripe fires payment_intent.succeeded webhook when money confirmed Webhook received — POST /webhooks: { "type": "payment_intent.succeeded", "data": { "object": { "id": "pi_3OqLk2..." } } } → Subscription status updated to "active" in APIForge database Card declined scenario: HTTP/1.1 402 Payment Required { "error": { "type": "payment_error", "code": "card_declined", "message": "Your card was declined. Please try a different card.", "param": "paymentMethodId" } }
What just happened?

The API returns 202 Accepted — not 200 OK. The difference matters: 202 tells the client the request was received and is being processed, but the final outcome is not yet known. The subscription is incomplete until the webhook fires. A 200 would imply the subscription is active immediately, which is incorrect for payment flows that require confirmation.

Stripe error codes like card_declined and insufficient_funds map cleanly to user-facing messages. The structured error response uses the same shape as the Stripe case study patterns — the frontend can read error.code to show the right UI state without parsing the message string.

Try this: Test the declined card path using Stripe's test card number 4000000000000002 — it always triggers a card_declined error in test mode, no real money involved.

Use Case 2 — Sending Slack Notifications Without Blocking the API

The APIForge Engineering team wants Slack alerts whenever a new project is created or a member is added. The naive approach is to call the Slack API synchronously inside the route handler — create the project, then POST to Slack, then return the response. But Slack's API can take 200–500ms to respond, and if Slack is down, every project creation fails with a 503.

The correct approach is to decouple the notification from the request. The route handler creates the project and immediately returns 201. A background job picks up the notification task and calls Slack asynchronously. If Slack is slow or down, the project creation still succeeds — the notification just gets retried later.

Notification Architecture — Decoupled
POST /api/projects
Route handler
201 Created
Immediate response
+
Queue job
Non-blocking
Worker picks up job
Background process
POST to Slack API
With retry on failure
// WHAT: APIForge Slack notification — async job pattern using BullMQ + Redis
// File: src/jobs/notificationJob.js + src/controllers/projects.controller.js
// Project creation returns immediately; Slack call happens in the background

import { Queue, Worker } from 'bullmq';

const notificationQueue = new Queue('notifications', {
  connection: { host: 'localhost', port: 6379 },
});

// ── Route handler — creates project and queues notification, returns fast ──
export async function createProject(req, res) {
  const { name, status } = req.body;

  const project = {
    id:        `proj_${generateId()}`,
    name,
    status:    status || 'active',
    createdBy: req.user.userId,
    createdAt: new Date().toISOString(),
  };

  await db.projects.insert(project);

  // Queue the Slack notification — does NOT await it
  await notificationQueue.add('slack-alert', {
    event:   'project.created',
    project,
    actor:   req.user.email,
  }, {
    attempts: 3,          // retry up to 3 times on Slack failure
    backoff:  { type: 'exponential', delay: 2000 },
  });

  // Return immediately — notification happens in background
  res.status(201).json({ success: true, data: project });
}

// ── Background worker — processes the notification queue ──────────────────
const worker = new Worker('notifications', async (job) => {
  const { event, project, actor } = job.data;

  const slackPayload = {
    text: `New project created by ${actor}`,
    blocks: [
      {
        type: 'section',
        text: { type: 'mrkdwn', text: `${project.name} was just created` },
        fields: [
          { type: 'mrkdwn', text: `Status:\n${project.status}` },
          { type: 'mrkdwn', text: `Project ID:\n${project.id}`  },
        ],
      }
    ],
  };

  const response = await fetch(process.env.SLACK_WEBHOOK_URL, {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body:    JSON.stringify(slackPayload),
  });

  if (!response.ok) throw new Error(`Slack API error: ${response.status}`);
}, { connection: { host: 'localhost', port: 6379 } });
POST /api/projects Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... Content-Type: application/json { "name": "Notification Service", "status": "active" } HTTP/1.1 201 Created ← returned in 8ms Content-Type: application/json { "success": true, "data": { "id": "proj_7Kp2mN", "name": "Notification Service", "status": "active", "createdBy": "usr_9f3k2m", "createdAt": "2024-11-14T10:30:00.000Z" } } Background worker log (runs ~50ms later): [worker] Processing job: slack-alert [worker] Sending to Slack: "Notification Service" created by priya@apiforge.dev [worker] Slack responded: 200 ok [worker] Job completed in 312ms Slack message received in #engineering channel: New project created by priya@apiforge.dev ┌─────────────────────────────────────┐ │ Notification Service was just created │ │ Status: active │ Project ID: proj_7Kp2mN │ └─────────────────────────────────────┘ Slack outage scenario — worker retries automatically: [worker] Attempt 1 failed: Slack API error: 503 [worker] Retrying in 2000ms (exponential backoff) [worker] Attempt 2 failed: Slack API error: 503 [worker] Retrying in 4000ms [worker] Attempt 3: Slack responded 200 ok — job completed
What just happened?

The route handler runs in 8ms. The Slack call takes 312ms. Because they are decoupled, the user gets their 201 response in 8ms regardless of what Slack does. The queue persists in Redis — if the API server restarts mid-notification, the job survives and the worker picks it up on restart. No notification is lost.

Exponential backoff means the worker waits 2 seconds before the first retry, 4 seconds before the second, 8 seconds before the third. This prevents a thundering herd — if Slack is down and you have 500 queued notifications, a fixed 1-second retry floods Slack the moment it comes back. Exponential backoff spreads the load.

Try this: Kill the worker process and create several projects. Restart the worker — watch it process all the queued jobs in order. That persistence is Redis doing its job.

Use Case 3 — Full-Text Search That Stays Fast

The APIForge platform has 50,000 projects across its customer base. A user types into a search box and expects results in under 100ms. The naive implementation is a SQL LIKE '%search term%' query — which performs a full table scan, ignores word boundaries, cannot rank by relevance, and slows down linearly as the table grows.

The production pattern uses a dedicated search index. When a project is created or updated, its data is written to both the database (source of truth) and a search index (optimised for text queries). Search requests hit the index, not the database. The index can return ranked, typo-tolerant results in under 10ms regardless of dataset size.

// WHAT: APIForge project search — search index pattern using Meilisearch
// File: src/controllers/search.controller.js
// Search hits the index, not the database. Index is updated on every write.

import { MeiliSearch } from 'meilisearch';
const search = new MeiliSearch({ host: process.env.MEILISEARCH_URL });
const index  = search.index('projects');

// ── Search endpoint — GET /api/search?q=gateway&status=active&limit=10 ────
export async function searchProjects(req, res) {
  const {
    q      = '',
    status,
    limit  = 20,
    offset = 0,   // offset is fine here — search results are stateless snapshots
  } = req.query;

  const filters = status ? `status = "${status}"` : undefined;

  const results = await index.search(q, {
    limit:             parseInt(limit),
    offset:            parseInt(offset),
    filter:            filters,
    attributesToHighlight: ['name', 'description'],  // wrap matches in <em> tags
    highlightPreTag:   '',
    highlightPostTag:  '',
  });

  res.json({
    query:       q,
    hits:        results.hits,
    total:       results.estimatedTotalHits,
    processingMs: results.processingTimeMs,
    limit:       parseInt(limit),
    offset:      parseInt(offset),
  });
}

// ── Keep index in sync — called from project create/update/delete routes ──
export async function syncProjectToIndex(project, operation = 'upsert') {
  if (operation === 'delete') {
    await index.deleteDocument(project.id);
    return;
  }
  await index.addDocuments([{
    id:          project.id,
    name:        project.name,
    description: project.description,
    status:      project.status,
    ownerEmail:  project.owner.email,
    createdAt:   project.createdAt,
  }]);
}
GET /api/search?q=gateway&status=active&limit=3 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... HTTP/1.1 200 OK Content-Type: application/json { "query": "gateway", "hits": [ { "id": "proj_8f3k2m", "name": "API Gateway Redesign", "description": "Refactor the gateway layer to support gRPC", "status": "active", "ownerEmail": "priya@apiforge.dev", "_formatted": { "name": "API Gateway Redesign", "description": "Refactor the gateway layer to support gRPC" } }, { "id": "proj_3n7q1p", "name": "Gateway Auth Migration", "description": "Move auth logic from services to the gateway", "status": "active", "ownerEmail": "sam@apiforge.dev", "_formatted": { "name": "Gateway Auth Migration", "description": "Move auth logic from services to the gateway" } } ], "total": 2, "processingMs": 4, "limit": 3, "offset": 0 }
What just happened?

4ms processing time for a full-text search across 50,000 documents. That is not the database — that is a search index that stores documents in an inverted index structure optimised for exactly this query pattern. A SQL LIKE '%gateway%' on the same data would take 200–800ms and get slower as the table grows.

Note that this use case is one of the few places where offset pagination is appropriate — search results are a stateless snapshot, not a live cursor through a changing dataset. A user paginating search results does not expect results to shift between pages because a new project was created mid-search.

Try this: Search for a misspelled term like "getway" — Meilisearch returns "gateway" results anyway. That typo tolerance is built into the index engine and requires zero extra code on your side.

Use Case 4 — File Uploads Without Routing Through Your Server

The APIForge Content team wants users to attach specification documents to projects — PDFs, diagrams, up to 50MB per file. The instinct is to accept the file in a multipart POST request, save it to disk, move it to S3, and return a URL. That works, but it routes every megabyte through your API server, consuming memory, CPU, and bandwidth for pure file transfer work.

The production pattern is a pre-signed URL. The client asks the API for a temporary upload URL that points directly to S3. The API generates this URL using AWS credentials and returns it to the client. The client uploads the file straight to S3 — the API server never touches the file bytes. The API gets notified via an S3 event when the upload completes.

// WHAT: APIForge file upload — pre-signed URL pattern with AWS S3
// File: src/controllers/uploads.controller.js
// Client uploads directly to S3 — API server handles only metadata

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl }               from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({ region: process.env.AWS_REGION });

// Step 1: Client requests a pre-signed upload URL
export async function getUploadUrl(req, res) {
  const { projectId, filename, contentType, fileSizeBytes } = req.body;

  // Validate file size before issuing the URL (50MB limit)
  if (fileSizeBytes > 50 * 1024 * 1024) {
    return res.status(413).json({
      error: { type: 'invalid_request_error', code: 'file_too_large',
               message: 'Files must be under 50MB.' }
    });
  }

  // Allowed types only
  const allowed = ['application/pdf', 'image/png', 'image/jpeg'];
  if (!allowed.includes(contentType)) {
    return res.status(415).json({
      error: { type: 'invalid_request_error', code: 'unsupported_media_type',
               message: `Content type ${contentType} is not permitted.` }
    });
  }

  const key = `projects/${projectId}/${Date.now()}-${filename}`;

  const command = new PutObjectCommand({
    Bucket:      process.env.S3_BUCKET,
    Key:         key,
    ContentType: contentType,
  });

  // URL expires in 5 minutes — client must upload within this window
  const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 });

  // Record the pending upload in the database
  const attachment = await db.attachments.create({
    projectId, key, filename, contentType, fileSizeBytes,
    status:    'pending',   // becomes 'ready' when S3 confirms upload
    uploadedBy: req.user.userId,
  });

  res.status(200).json({
    uploadUrl,                            // client POSTs file bytes here
    attachmentId: attachment.id,          // client sends this on completion
    expiresAt:    new Date(Date.now() + 300000).toISOString(),
  });
}
Step 1 — Client requests upload URL: POST /api/projects/proj_8f3k2m/attachments/upload-url Content-Type: application/json { "filename": "api-spec-v2.pdf", "contentType": "application/pdf", "fileSizeBytes": 2457600 } HTTP/1.1 200 OK { "uploadUrl": "https://apiforge-files.s3.amazonaws.com/projects/proj_8f3k2m/1731580800-api-spec-v2.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=...&X-Amz-Expires=300&X-Amz-Signature=9f3a2b8c...", "attachmentId": "att_7Kp2mN", "expiresAt": "2024-11-14T10:35:00.000Z" } Step 2 — Client uploads directly to S3 (no API server involved): PUT https://apiforge-files.s3.amazonaws.com/projects/proj_8f3k2m/... Content-Type: application/pdf [2.4MB file bytes] HTTP/1.1 200 OK ← from S3, not APIForge Step 3 — Client notifies APIForge the upload is complete: POST /api/attachments/att_7Kp2mN/confirm Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... HTTP/1.1 200 OK { "id": "att_7Kp2mN", "filename": "api-spec-v2.pdf", "url": "https://apiforge-files.s3.amazonaws.com/projects/proj_8f3k2m/1731580800-api-spec-v2.pdf", "status": "ready", "sizeBytes": 2457600 } File too large scenario: HTTP/1.1 413 Content Too Large { "error": { "code": "file_too_large", "message": "Files must be under 50MB." } }
What just happened?

The API server handled two tiny JSON requests — the URL request and the confirm request. The 2.4MB PDF went directly from the user's browser to S3. At scale, this is the difference between an API server that needs 32GB of RAM to handle concurrent uploads and one that runs on 512MB. The API never buffers file bytes in memory.

The 5-minute URL expiry is a security control. A pre-signed URL is a capability token — anyone who has it can upload to that specific S3 path. Keeping the TTL short limits the window of exposure if the URL leaks. The pending status on the attachment record means the file is tracked even if the client crashes mid-upload — a cleanup job can expire pending attachments older than 10 minutes.

Try this: Set the URL expiry to 5 seconds, request a URL, wait 10 seconds, then try to upload. S3 returns a 403 — the signature has expired. That is the expiry mechanism working as intended.

Use Case 5 — Integrating With GitHub via OAuth and Webhooks

The APIForge Engineering team wants to connect projects to GitHub repositories. When a pull request is merged, the linked APIForge project status should update automatically. When a user connects their GitHub account, APIForge needs read access to their repositories — but it must never ask for more permissions than necessary and must never store the user's GitHub password.

Two API patterns solve this together: OAuth 2.0 handles the "connect your GitHub account" flow — GitHub issues a time-limited access token to APIForge without the user ever sharing their credentials. Webhooks handle the real-time sync — GitHub calls an APIForge endpoint whenever a pull request merges, and APIForge updates the project status in response.

OAuth in plain English
OAuth is a delegated authorization protocol. The user tells GitHub "I allow APIForge to read my repositories." GitHub gives APIForge a token that proves this consent. APIForge uses that token to make API calls on the user's behalf — but only for the scopes the user approved. The user can revoke this access from GitHub's settings at any time, immediately invalidating the token.
// WHAT: APIForge GitHub integration — OAuth token exchange + webhook handler
// File: src/controllers/github.controller.js
// OAuth flow: redirect → callback → exchange code for token → store token

// Step 1: Redirect user to GitHub to approve access
export function initiateOAuth(req, res) {
  const params = new URLSearchParams({
    client_id:    process.env.GITHUB_CLIENT_ID,
    redirect_uri: 'https://api.apiforge.dev/integrations/github/callback',
    scope:        'repo:status read:user',  // minimum required scopes only
    state:        generateCsrfToken(req),   // prevents CSRF attacks on the callback
  });
  res.redirect(`https://github.com/login/oauth/authorize?${params}`);
}

// Step 2: GitHub redirects back with a one-time code — exchange for access token
export async function handleOAuthCallback(req, res) {
  const { code, state } = req.query;

  if (!verifyCsrfToken(req, state)) {
    return res.status(400).json({ error: 'Invalid state parameter — possible CSRF attack' });
  }

  const tokenResponse = await fetch('https://github.com/login/oauth/access_token', {
    method:  'POST',
    headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
    body:    JSON.stringify({
      client_id:     process.env.GITHUB_CLIENT_ID,
      client_secret: process.env.GITHUB_CLIENT_SECRET,
      code,
    }),
  });

  const { access_token, scope, token_type } = await tokenResponse.json();

  // Store the encrypted token — never log or expose it
  await db.integrations.upsert({
    userId:          req.user.userId,
    provider:        'github',
    accessToken:     encrypt(access_token),  // encrypted at rest
    scopes:          scope,
    connectedAt:     new Date().toISOString(),
  });

  res.redirect('/settings/integrations?github=connected');
}

// Step 3: GitHub webhook fires when a PR merges — update project status
export async function handleGithubWebhook(req, res) {
  // Verify signature (same HMAC pattern as Lesson 34)
  verifyGithubSignature(req.body, req.headers['x-hub-signature-256']);

  const event   = req.headers['x-github-event'];
  const payload = JSON.parse(req.body);

  if (event === 'pull_request' && payload.action === 'closed' && payload.pull_request.merged) {
    const repoFullName = payload.repository.full_name;
    const project      = await db.projects.findByRepo(repoFullName);

    if (project) {
      await db.projects.update(project.id, {
        lastMergedAt: new Date().toISOString(),
        prCount:      project.prCount + 1,
      });
    }
  }

  res.status(200).json({ received: true }); // always acknowledge quickly
}
OAuth Flow: Step 1 — User clicks "Connect GitHub" in APIForge: GET /integrations/github/connect → Redirects to: https://github.com/login/oauth/authorize?client_id=Iv1.abc123&scope=repo:status+read:user&state=csrf_8fKp2m Step 2 — User approves on GitHub, redirected back: GET /integrations/github/callback?code=3a8f2b1c9d4e&state=csrf_8fKp2m → State verified ✓ → Token exchanged with GitHub API → Access token stored (encrypted) in database HTTP/1.1 302 Found Location: /settings/integrations?github=connected Step 3 — PR merged in connected repo, GitHub fires webhook: POST /webhooks/github X-GitHub-Event: pull_request X-Hub-Signature-256: sha256=9f3a2b8c4d1e6f7a... Content-Type: application/json { "action": "closed", "pull_request": { "merged": true, "title": "Add rate limiter to gateway" }, "repository": { "full_name": "apiforge-team/gateway-service" } } → Signature verified ✓ → Project proj_8f3k2m found via repo link → lastMergedAt and prCount updated HTTP/1.1 200 OK { "received": true } Token revocation (user disconnects GitHub from APIForge settings): DELETE /integrations/github → Token deleted from database → APIForge can no longer call GitHub on this user's behalf
What just happened?

The CSRF state parameter is not optional. Without it, an attacker can craft a URL that tricks a logged-in user into connecting their GitHub account to the attacker's APIForge account. The state token ties the OAuth callback to the specific browser session that initiated the flow — a callback with a state that does not match the session is rejected.

The GitHub access token is encrypted before storage — not just hashed. Hashing is one-way, meaning you cannot retrieve the original token to make API calls. Encryption is reversible using a server-side key, so the token can be decrypted when needed for GitHub API calls. The encryption key lives in an environment variable, not in the database.

Try this: After connecting GitHub, immediately check what scopes were actually granted by calling GET https://api.github.com/user with the access token and reading the X-OAuth-Scopes header in the response — GitHub tells you exactly what the token can do.

Patterns Across All Five Use Cases

Every use case in this lesson applied patterns you have already built. The services changed — Stripe, Slack, S3, GitHub — but the underlying decisions were the same ones you made in earlier lessons. This is what experience with APIs actually looks like: not memorising service-specific SDKs, but recognising which pattern applies to which problem.

Naive Approach
Retry payments without idempotency → duplicate charges
Call Slack inside the route handler → Slack outage breaks project creation
LIKE '%query%' in SQL → 800ms search, gets slower each week
Route file bytes through API server → memory exhaustion at scale
Ask users to paste GitHub tokens → security incident waiting to happen
Production Pattern
Idempotency key on every payment → safe to retry indefinitely
Background job queue → Slack failure never touches API response
Search index → 4ms queries regardless of dataset size
Pre-signed S3 URLs → API server never handles file bytes
OAuth 2.0 → users grant scoped access, revocable at any time
Use Case Key Pattern Applied Lesson Reference Critical Decision
Payments Idempotency keys + webhook activation Lesson 34 (idempotency) Activate on webhook, not charge response
Notifications Async job queue + exponential backoff Lesson 30 (async jobs) Decouple notification from request path
Search Search index + query parameters Lesson 14 (filtering) Index is separate from database
File Uploads Pre-signed URLs + async confirmation Lesson 30 (async pattern) Files never touch the API server
Third-Party OAuth OAuth 2.0 + signed webhooks + encryption Lessons 20, 34 (OAuth, webhooks) Encrypt tokens at rest, CSRF on callback
The pattern underneath everything: Every production API integration follows the same shape — validate input, perform the minimum synchronous work needed to return a useful response, hand off anything slow or failure-prone to an asynchronous system, and verify the authenticity of every inbound call from a third party. The services change. Stripe becomes Braintree. Slack becomes Teams. S3 becomes Google Cloud Storage. GitHub becomes GitLab. The patterns do not change.

Quiz

1. The APIForge billing endpoint returns a 202 and a clientSecret after a subscription is created. A developer suggests activating the Pro plan immediately when the 202 is received. Why is this wrong?

2. A developer asks why the APIForge file upload endpoint returns a URL instead of accepting the file directly. What is the correct explanation of the pre-signed URL pattern?

3. The APIForge GitHub OAuth implementation includes a state parameter in the authorization URL and verifies it on the callback. What attack does this prevent?

Up Next
Project Planning
The APIForge Product team scopes the final project — a full API with auth, rate limiting, versioning, and webhooks — breaking it into phases with clear deliverables before a single line of code is written.