Module 2 · APIs

2.4 API security: what actually breaks, and how not to be the one who breaks it

Lesson 2.4 · 12 min read

Let's be honest about something uncomfortable. The gap between an app that works and an app that's safe to put in front of real people is larger than most builders realize, and almost none of it is visible when you're clicking around your own creation. Your app looks fine. It does what you asked. And it can still be leaking every user's data to anyone who thinks to look.

This isn't fear-mongering, and it isn't a reason to slow down. It's the reason to understand a handful of specific, concrete failures that account for most of what actually goes wrong. Security in practice isn't an infinite ocean of exotic attacks. It's a fairly small number of mistakes made over and over, and once you know them by name and can spot them, you'll catch them in your own work and in whatever an AI hands you.

That's what this post gives you: the real failures, why each one happens, and what correct looks like. Not to make you a security expert. To make you someone who doesn't ship the obvious holes, which is a genuinely meaningful bar, and one an enormous number of shipped applications fail to clear.


The mindset that has to shift first

Before the specifics, one change in how you see your own product, because everything else follows from it.

When you build something, you naturally think about how it's meant to be used. The user opens the page, fills in the form, clicks the button. That's the path you designed and the path you test. Security thinking asks a different question: what happens if someone uses it in a way you never intended?

Because here's the thing every builder has to internalize. Your frontend is a suggestion, not a boundary. It runs on the user's own machine, and they can change it, bypass it, or ignore it entirely. That form field with a maximum length? Someone can send a request without it. That admin button you hid from regular users? The endpoint it calls is still there, and anyone can call it directly. That validation you wrote in the browser? It never ran, because they never used the browser, they sent the request straight from a tool.

This is the single most important idea in all of API security, so let it settle. Anyone can send any request, to any of your endpoints, with any data they choose, at any time, no matter what your interface allows. The interface constrains only the people using the interface. The API is the real front door, and it must defend itself as though the frontend does not exist, because for an attacker, it doesn't. Every rule you care about must be enforced on the server. Every check that matters must happen where the user cannot reach it.

Once you truly hold that, the specific failures below stop being a list to memorize. They become obvious consequences of forgetting it.


Failure one: the exposed secret

We touched this in the last post, but it's the most frequent serious mistake in AI-assisted building, so it earns a full accounting.

Your app needs to call some service, an AI provider, a payments processor, an email sender, and that service authenticates you with an API key. You're building fast, the AI writes a component that calls the service directly from the frontend, and it works perfectly. You ship it. And you have published your secret key to the entire internet.

Why? Because everything in your frontend is downloaded to the user's machine to run. All of it. The code, the keys sitting in it, everything. A person can open the very DevTools you learned in the last module and read your key in plain sight. Worse, they don't even need to look: automated systems continuously scan public code and live sites for exposed keys, find them within minutes, and use them. This is one of the most common ways people wake up to enormous bills or drained quotas.

The correct shape is the one we named before, and it's worth restating as a permanent rule. The secret lives on your server, where users cannot see it. Your frontend calls your backend. Your backend, holding the secret privately, calls the external service and returns the result. The user gets what they need, the key is never sent to their device, and you keep control. When you ask an AI to integrate any service that requires a key, this is what you must check: is this call happening on the server, or did it just put my key in code that ships to the browser?

And a related habit that costs nothing: secrets never get written directly into code files. They live in environment variables, a separate place for configuration values, kept out of your code and out of your version control. We'll cover this properly in the deployment module, but the principle is simple: code gets shared, committed, and pushed to places like GitHub. Secrets must never travel with it. A key committed to a public repository is a key that's already compromised, and no amount of deleting it later fully undoes that.


Failure two: trusting what the client sends

This is the direct consequence of "the frontend is a suggestion," and it's the source of an entire family of vulnerabilities.

Picture a checkout. The frontend calculates the total, then sends a request to the backend: create an order for these items, total $29. Feels reasonable. But the price came from the client, which means the user could have changed it to $0.29 before sending. If your backend accepts that number and charges it, you've just been robbed with nothing more sophisticated than editing a request. The fix is the principle in action: never trust data from the client for anything that matters. The backend should take the item ids, look up the real prices in its own database, and calculate the total itself. The client says what to buy. The server decides what it costs.

The same shape appears everywhere once you see it. A request to update a profile that includes a field saying "role": "admin", and a backend that obediently saves whatever fields it was sent. Now anyone can make themselves an admin. A request to transfer money that includes which account it's coming from, and a backend that doesn't verify the account belongs to the sender. A request to delete a post that includes the post id, and a backend that deletes it without checking who owns it, which is the authorization failure from the last post wearing a different hat.

Notice the through-line. In each case the backend took a claim from the client and treated it as a fact. The discipline is to ask, for every piece of data arriving in a request: is this something the client is allowed to decide, or is it something the server must determine or verify? What items to buy: the client decides. What they cost: the server determines. Which post to delete: the client indicates. Whether they may delete it: the server verifies. That single question, applied to every field of every request, prevents an enormous share of real vulnerabilities.

And this extends to validation generally. Any check about what's acceptable, is this a valid email, is this number within range, is this text too long, must exist on the server, whatever the frontend also does. Frontend validation is a kindness to users, giving them fast feedback. It is not protection. It never was. The server is the only place a rule is actually enforced.

Interactive: client vs server trust

A hands-on visual for this idea is in the works. The text around it carries everything you need for now.


Failure three: leaking more than you meant to

This one is quieter than the others and extremely common, especially in code written quickly.

An endpoint fetches a user and returns it. The user record in the database contains their name, email, profile picture, and also their hashed password, their phone number, their internal notes, whether they're flagged for review. The controller does response.json(user) and sends the whole thing. The frontend only displays the name and picture, so everything looks perfect. But the entire record, including everything you never intended to reveal, was sent across the network and is sitting right there in the response, visible to anyone who opens the network tab.

The frontend choosing not to display something is not the same as the data not being sent. This is the same illusion as before, in another costume: what the interface shows is unrelated to what the API returns. If it's in the response, it's public to whoever asked.

The fix is a habit rather than a tool. When an endpoint returns data, return only the fields that are actually needed for that purpose, chosen explicitly. Not "here's the whole record," but "here's the name, avatar, and bio." This is sometimes called returning a shaped or filtered response, and it means you decide, deliberately, what leaves your server. It takes a few extra lines and it prevents entire categories of leaks, including the ones you'd never have thought to worry about, like a field someone adds to the database next year that suddenly starts riding along in every response.

So when you're reviewing an endpoint, yours or an AI's, look at what it returns and ask: is every single field in this response something I'm comfortable being read by anyone who can call this endpoint? If a field makes you hesitate, it shouldn't be there.


Failure four: no limits on who can ask, or how often

Every endpoint you expose can be called. Repeatedly. Rapidly. By anyone, or by a script, thousands of times a second.

Think about what that enables if nothing stops it. A login endpoint with no limits lets someone try millions of password guesses against an account. A password reset endpoint lets someone spam a user with emails. An expensive endpoint, one that runs a heavy database query or, very relevantly today, calls an AI model that costs you money per request, can be hammered until your bill explodes or your service collapses under the load.

The defense is rate limiting: a rule that caps how many requests a given caller can make in a given period. Five login attempts per minute. A hundred API calls per hour. Past that, the server refuses with a status code meaning "too many requests" and the flood stops. Most serious backends put this in front of everything, and it's essential in front of anything expensive, anything that sends messages, and anything related to authentication.

This is worth genuine attention for AI-powered products specifically, because the economics are unusual and unforgiving. If your app calls an AI model on each request and you have no rate limit, a single motivated person, or a broken script, can generate an enormous bill in minutes while you sleep. The endpoint works perfectly. It does exactly what you built it to do, as many times as it's asked. That's the whole problem.


Failure five: forgetting the transport

A shorter one, but it's foundational and now you have the pieces to understand it properly.

Requests travel across networks you don't control: coffee shop wifi, mobile networks, the open internet. If a request travels as plain readable text, anyone positioned along that path can read it. Including the authentication credential riding in its headers. Someone reads that token, attaches it to their own requests, and is now indistinguishable from your user.

This is what HTTPS prevents, the encrypted version of HTTP we met in the very first post, the sealed envelope rather than the postcard. It scrambles the entire conversation so that anyone intercepting it sees only noise. Every request carrying anything that matters, which in practice means every request, must travel over HTTPS. This is well-handled by modern platforms and usually free, so the failure here is rarely technical, it's forgetting to enforce it, or allowing an unencrypted path to remain open as a fallback. The rule is simple: HTTPS everywhere, always, with no plain HTTP path left available.


The habit that ties it together

Five failures: the exposed secret, trusting the client, leaking extra data, no rate limits, and unencrypted transport. Together with the authentication and authorization gaps from the last post, these account for a very large share of what actually goes wrong in real applications. Not exotic attacks. These.

And they share one root, the mindset we started with. Every one of them comes from imagining your API is only ever called by your own beautiful interface, by users behaving as designed. The correction is to picture your API standing alone, with no frontend at all, being called directly by someone curious and unfriendly who can send anything they like. What would they find? What could they read that isn't theirs? What could they claim about themselves that you'd believe? How many times could they call the expensive thing? Would your secrets be sitting there? That imagined visitor is the review you should run on everything you build, and it takes about two minutes.

This matters more, not less, when you build with AI. AI writes code that works, and working is what it optimizes for. It will happily produce an endpoint that returns the whole user record, calculates the price from what the client sent, has no rate limit, and puts the API key wherever it's convenient. All of that will function flawlessly when you test it. None of it will be safe. The AI is not being careless; it's doing exactly what you asked, and you didn't ask for the invisible half. The person who has to ask for the invisible half is you, and you can only ask for what you know to look for.

That's what this post was for. You now know what to look for. Which means you can build fast, use AI hard, and still not be the person who shipped the thing that leaked everyone's data, and that combination, speed plus judgment, is exactly what a design engineer is.


Do this before the next post

Take something you've built with AI, or have an AI build a small backend with a few endpoints and a service integration. Then sit down and run the imagined-visitor review, deliberately, on real code.

Go endpoint by endpoint and ask the five questions. Is any secret or API key present in code that ships to the browser? Does any endpoint accept a value from the client, like a price, a role, or an owner id, and act on it without verifying it against the server's own truth? Does any response include fields that don't need to be there, and would you be comfortable if a stranger read every field in it? Is there any limit on how often the expensive or sensitive endpoints can be called? And is everything traveling over HTTPS with no unencrypted path?

Write down what you find. You will find something, and that's the point, not a failure. Every builder does, on their first honest look at their own work. What matters is that you can now see it, which means you can fix it, which means the thing you ship is something you'd be comfortable putting your name on. That's the whole difference between building things and building things properly.