Skip to main content
Resume
View Resume

APIs in Practice: What Working With Them Actually Teaches You

What working with real APIs -- Google Calendar, Supabase, Cloudflare R2 -- taught me about authentication, OAuth flows, and the gap between reading documentation and actually shipping an integration.

Reading API documentation and actually shipping an integration are two different experiences. The documentation makes the concepts look clean. Real integration tends to surface the parts that are less clean -- auth flows that don't work exactly as described, edge cases in error handling, rate limits that only become relevant once you're actually running.

These are notes from building real integrations: Google Calendar, Google Contacts, Cloudflare R2, and Supabase in several projects over the past year.

The basics, stated plainly

An API is a way to talk to someone else's server. Your code sends an HTTP request, their server does something, and it sends back a response. The response is usually JSON.

That's the whole model. Everything else is implementation detail around authentication, what the specific endpoints accept, and what their responses look like.

Authentication

Most APIs require authentication. The two patterns I've worked with most are API keys and OAuth.

API keys are simple: the service gives you a long string, you include it in every request header, the server checks it. For Cloudflare R2, Supabase's service role operations, and similar cases, this is how it works.

headers = {
    "Authorization": f"Bearer {api_key}",
    "Content-Type": "application/json"
}
response = requests.get("https://api.example.com/data", headers=headers)

The critical practice: never put API keys in source code. Environment variables are the right place. Every platform I've deployed on (Vercel, Netlify, Cloudflare Pages) has a dashboard for managing environment variables that are injected at runtime. This keeps secrets out of git history.

OAuth is more involved. It exists for cases where your app needs to access data belonging to a specific user -- like reading their Google Calendar -- and the user has to explicitly authorize it.

The flow in plain terms:

  1. Your app sends the user to Google's authorization page
  2. The user reviews the permissions and clicks Allow
  3. Google redirects back to your app with a temporary authorization code
  4. Your app exchanges that code for an access token and a refresh token
  5. You use the access token for API calls (it expires, usually after an hour)
  6. When it expires, the refresh token silently gets you a new access token without the user doing anything

Implementing this correctly the first time takes longer than you expect. The documentation describes each step but the integration points between steps -- how to store the refresh token, how to detect expiration, how to handle the user revoking access -- those are the parts you figure out while building.

Working with Supabase

Supabase combines a database (Postgres), authentication, and a REST API layer in one service. A few things about it that took time to understand properly:

Row Level Security (RLS): You write policies on database tables that restrict which rows each user can access. When your backend creates a Supabase client and attaches the user's JWT to it, the database enforces those policies automatically. Every user's queries are scoped to their own data.

The pattern for making this work in Python:

# Per-request client with the user's token
client = create_client(SUPABASE_URL, SUPABASE_KEY)
client.postgrest.auth(access_token)  # This activates RLS

Without this, all queries run under the anonymous key and bypass RLS -- meaning you'd return data across all users. Getting this right is the difference between a secure multi-user app and one where any user can see everyone else's data.

PostgREST query syntax: Supabase's Python client wraps an API called PostgREST. Instead of writing SQL, you chain method calls:

result = supabase.table("people")\
    .select("*")\
    .eq("user_id", current_user.id)\
    .eq("is_deleted", False)\
    .order("created_at", desc=True)\
    .execute()

This is mostly intuitive once you get used to it. The .or_() method for OR conditions and .contains() for array fields required more reading.

Working with Cloudflare R2

R2 is Cloudflare's object storage -- conceptually similar to AWS S3, but with no egress fees, which matters for files accessed frequently.

R2 is S3-compatible, which means any library that works with S3 also works with R2. In Python, that's boto3:

s3 = boto3.client(
    "s3",
    endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com",
    aws_access_key_id=access_key_id,
    aws_secret_access_key=secret_access_key,
)

# Upload a file
s3.put_object(Bucket=bucket_name, Key=file_path, Body=file_bytes, ContentType="image/jpeg")

# Delete a file
s3.delete_object(Bucket=bucket_name, Key=file_path)

The R2 bucket serves files publicly through a custom CDN URL you configure separately. So the upload path and the public access URL are different, which is easy to overlook when you're first setting things up.

Rate limits and error handling

Every API has rate limits. You find out about them when you hit them -- a 429 response ("Too Many Requests"). The practical response is to understand what the limit is, design around it (batch where possible, cache things that don't change), and handle 429 gracefully rather than crashing.

Error handling is the part of API integration that's easiest to write badly. The happy path is straightforward -- request succeeds, handle the data. The failure cases are where the real work is: the API is down, the token expired, the network timed out, the rate limit was hit, the resource doesn't exist.

For anything that touches an external service in a user-facing feature, I try to wrap the call in error handling and give the user a clear message rather than a crash. For background operations (like Google Calendar sync in KnowThem), failures log but don't block the main operation.

The gap between documentation and implementation

Documentation describes what an API does. It doesn't usually describe how to handle the cases where it doesn't behave as described.

The things I've found that only come out in practice: how long OAuth callback handling actually takes under load, what "rate limit exceeded" errors look like from that specific API versus a generic 500, how refresh token rotation works (some services issue a new refresh token each time, invalidating the old one), and what happens when you make a request while the user is in the middle of revoking authorization.

Building real integrations is what teaches you these things. The documentation is where you start, not where you end.