Article summary
Most apps today draw a strong line between the server and the client. The client, maybe a single-page web application or a native mobile app, focuses on the user-facing features, while the server provides the data and a way to update it. Atomic has done a lot of projects this way, and we’ve found it’s a solid way to decouple the core business logic and database from different platforms.
Decoupling the client from the server means we can use an alternative back-end instead of the eventual production server. Several projects I’ve worked on have used this ability to keep development on the client moving forward, even though the actual production back-end wasn’t ready yet. Ryan wrote about the benefits of fake APIs like this a couple of years ago. But there’s more to swappable back-ends than fake APIs. Different situations call for different servers with different powers.
Below are four types of servers, each behaving differently in order to suit a specific need and provide particular tools. All of them must expose the same interface so clients can use them, blissfully unaware of the specific server-side behavior. From now on, I’ll refer to these servers as APIs.
1. Production API
Eventually, every app will need a production API. This is the server that sends real data from the database, makes updates to it, and enforces all the permissions and business logic. It shouldn’t return data to users who shouldn’t see it, and it doesn’t allow users to update information they don’t have permission to change. Production APIs need to be fast and scalable, because they’ll handle all the app’s real-world traffic.
Most apps already have a production API, so there isn’t any need to go into more detail on them.
2. Development API
Development APIs are what we at Atomic have traditionally called “fake APIs,” but I don’t like the term “fake” because it’s vague and can mean different things to different people.
A development API is one that stands in for the production API like a body double: All it has to do is look the part. Development APIs are perfect for working on the client before the production API is available, allowing front-end progress to continue, even though there’s no real data or database backing it yet. Development APIs usually don’t persist data to a database and just keep everything in memory. It’s often useful to have some example data that loads when the server starts.
It’s tough to say how much logic a development API should have–that is, how much it should mimic the production API. Different developers have different tastes. Some prefer a minimal API that doesn’t worry about form validations, data integrity, or page-to-page consistency. Others want an API that creates a fuller illusion of having a real back-end, one they could use to give a reasonably accurate demo of the app. What works best is up to you.
But even if your production API is available, development APIs still have a major advantage: tooling. I like to run my development API from a REPL so I can change its behavior on the fly. Rather than searching for a place in the app that triggers a server response of 404 Not Found
, I can simply tell the server to respond that way on the next request. In a web app, getting a 404 is as easy as changing the URL, but it can be much more difficult in a mobile app. It can also be tricky to manipulate the server into giving other kinds of responses, such as 400 Bad Request
, 401 Unauthorized
, 403 Forbidden
, and 500 Internal Server Error
. A development API that lets you configure the API’s responses is much more direct and transparent.
Being able to tailor individual server responses also allows you to delay response times to simulate heavy server loads, poor performance, or slow connectivity. A development API running on your local machine with an in-memory database usually responds too fast to show the UI’s pre-data state, what the loading animations look like, or how the client handles requests that time out. Being able to stall responses from the API means you don’t have to make awkward hacks to demonstrate these states.
One misuse of a development API involves running tests against it. Full-stack or end-to-end tests should be run against the production API with a non-production database. Even tests with more limited scope shouldn’t use the development API because it encourages adding functionality that developers don’t need. Unused functionality is dangerous because it’s usually neither tested nor documented, and it tends to create confusion.
But more importantly, using a development API for testing leaves important information out of the tests. When it comes to testing, I recommend that you use a mock API.
3. Mock API
Mock APIs are the simplest of the APIs listed here. I’ve written a version in Go with only 200 lines, and an even shorter one in JavaScript. Neither has any external dependencies because neither does anything other than listen for what to mock and return it when the request is made. They don’t have any application-specific logic.
A mock API works just like the mock objects you may have used when writing unit tests: The test doesn’t depend on any logic in the thing that’s been mocked. Instead, the test code specifies what the mocked object’s methods will be called, and, when they’re called, what they return. A mock API works the same way, requiring that tests tell it which API endpoints will be hit, with what data, and what gets returned.
There are several reasons why I prefer to run tests of the client against a general-purpose mock API. For starters, a mock API is much simpler. When I replaced our project’s testing API with one that behaved more like mocking an object, I eliminated not only 500 lines of application-specific code, but also all of the testing server’s mutable database and the logic that managed it–a poorly architected state that was difficult to understand and made debugging 20 tests take four days. The mental model of a mock API is much easier for developers to reason about.
A big part of that understanding comes from how transparent tests become when using a mock API. There’s no implicit setup when the server starts, and no need to guess what various calls to the API are doing, whether it’s replacing the current data, appending to it, or something else altogether. Instead, all the setup is explicit. The test specifies what endpoints the code that follows will hit and what the server will return. This technique co-locates the test data from the server with the assertions that will be made in the client, allowing developers to see clearly how the two are related.
Mock APIs also handle new scenarios more easily than a custom test API. You don’t have to configure the test API to handle URLs differently; you just mock what you want. Neither do you have to change a mock API’s code when you want to return a different status code or handle an edge case; you just mock the API call accordingly.
Lastly, a mock API is a general program that you can use from project to project without changing it. That reusability adds consistency between projects, removes the need to spend project time writing a custom testing API, and keeps onboarding developers who already know the mock API protocol from having to learn another project-specific tool.
4. Fuzzing API
The final type of API I’ll recommend today is the fuzzing API. I described it briefly in my post about testing CSS. Rather than behaving predictably, a fuzzing API does the unexpected. It returns random, inconsistent data and exposes how the client copes. Does it ignore problems and merrily continue on? Does it throw an error? Does it give the user a meaningful message? Does it leave any logs for developers? Fuzzing APIs do a much better job than humans at running into problematic edge cases and testing our implicit assumptions.
The simplest fuzzing API is one that returns data in the shape the client expects, but with random data. It’s a shallow API that doesn’t have any logic other than random data generation. But fuzzing APIs can also be more sophisticated, returning different response status codes, error messages, and even data in structures the client doesn’t use or content types it doesn’t understand. It can also respond after random delays, demonstrating how the app looks when UI data doesn’t load all at once or surfacing race conditions that don’t usually occur in development.
Because fuzzing APIs use random data to generate problem states, they need one more critical feature: the ability to stop being so random. When a fuzzing API illustrates a bug, you need to be able to freeze its current behavior and find out what caused the problem. Sometimes, it can take several tries to pinpoint why an error occurred. Once you know, you can write a test for it, but it’s tough to know what’s going on when it only happens once. Being able to suspend the randomness and do exactly what happened last time is an important part of a good fuzzing API.
Conclusion
Alternative APIs are an important tool for every developer to have in their toolbox. They allow us to do much more than a plain production API does, from customizing responses to improving how tests are expressed and helping find problems we didn’t even know to look for. I’ve found that alternative APIs dramatically improve my productivity and give me more flexibility when developing applications.
If you’ve used other types of APIs, I’d love to hear about them.