REST APIs
Software Mile designs and builds REST APIs – the connective tissue between your systems, apps, and partners. A good API is the difference between systems that integrate and systems that are integrated painfully, once, by hand.
APIs Built to Be Used
- Well-designed REST APIs with clear resources, versioning, and documentation
- Authentication, rate limiting, and security appropriate to who consumes them
- Integration APIs that connect your systems to each other and to partners
- GraphQL where a graph fits the client better than REST
An API is a contract other developers depend on – we design them to be clear and stable. Tell us what needs to talk to what.
Do You Need an API, or Just a Data Feed?
Plenty of integrations do not need one. If an internal system needs yesterday’s orders once a night, a scheduled export to a file or a database view is simpler to build and easier to debug when it fails overnight. It still moves data and still holds credentials, so it still belongs in a security review, but there is less surface area to review. APIs earn their cost when the consumer needs current data on demand, and when there will plausibly be more than one consumer over time.
A fair number of API projects begin life as a request for a report. Naming the actual need, which means freshness, volume, and who calls it and how often, tends to settle the question before the design hardens.
The Consumer Decides the Design
Almost every other decision follows from this one. An API used by one team you can walk over to is a different product from one used by partners under contract, which is different again from a public one. Getting it wrong in either direction is expensive: over-engineering an internal call, or shipping a partner interface with no versioning story.
- Internal only. Simpler auth, faster iteration, and a breaking change you can coordinate in a meeting.
- Partner facing. Versioning, a deprecation policy, per-consumer credentials and quotas, and documentation someone outside your building can follow unaided.
- Public. All of the above plus abuse protection, self-service key issuance, and the assumption that anything you ship is effectively permanent.
- Machine to machine inside one data center. Latency and throughput matter more than human-readable design, which may point away from REST entirely.
REST, GraphQL, or Something Else?
REST fits when resources are stable, consumers are varied, and caching and ordinary tooling matter. GraphQL fits when clients need different shapes of the same data and you are tired of shipping a new endpoint per screen, but you take on query cost control, caching complexity, and a schema that becomes its own governance problem.
There are also cases where neither is right. Event streams and webhooks suit “tell me when something happens” far better than polling ever will, and a bulk file transfer still beats paginating through millions of rows. Let the traffic pattern pick the shape.
Where Integration Cost Actually Lands
The happy path is rarely the problem. Integration cost concentrates in the details that surface once a real consumer is retrying in the middle of the night and somebody has to reconstruct what went wrong from a log file. These are worth checking on any API that already exists.
- Error responses. A 500 with an HTML body tells the caller nothing. Consistent, machine-readable errors with stable codes let a consumer diagnose without opening a ticket.
- Retries and idempotency. Anything that creates or charges needs an idempotency key, or a network timeout becomes a duplicate order.
- Pagination. Offset pagination breaks quietly on changing data. Cursors take more work to build and much less to live with.
- Documentation drift. Hand-written docs describe the API you meant to build. Generating them from the specification, and testing against it, keeps them honest.
- No correlation IDs. Without a request identifier carried through the logs, “it failed yesterday” is an unanswerable support ticket.
How Do You Version Without Breaking Everyone?
The easiest version is the one you never have to ship. Adding optional fields, tolerating unknown ones, and never changing the meaning of an existing field will carry an API a long way. Reserve a real version bump for changes that genuinely cannot be made additively, and expect to run both versions in parallel for as long as your slowest consumer needs.
That means deciding up front how you will tell consumers a version is going away, and how long they get. A deprecation policy written before you have consumers is a short internal document. Written afterward, it becomes a negotiation with everyone already depending on you. The contract outlives the implementation, which is the argument for writing the contract carefully.
The Specification Is the First Deliverable
The order that works is: agree resources and fields with whoever will call the API, publish the specification, stand up a mock server generated from it so the consumer can build against a fake while the real one is being written, then implement behind that contract and run a contract test in CI. Renaming a resource in YAML is free; renaming it after two consumers have shipped against it is a coordinated release.
OpenAPI 3.x describes paths, shapes, and status codes well, and says nothing about behavior. Ordering guarantees, which errors are safe to retry, how long a resource remains readable after deletion, and what happens when the same request arrives twice all sit outside the schema, and a consumer will assume something about each of them if you do not write it down. Generating reference documentation from the specification keeps the field lists honest and leaves the behavioral contract to prose, which has to be versioned alongside it.
A specification is worth writing at the level of detail a stranger could implement a client from, which is further than most first drafts go.
- Which status codes each operation can actually return, with the schema of the error body, not only the success case. A consumer writes its error handling from that list, and anything omitted gets handled by crashing.
- Whether a field can be null, absent, or an empty string, and which of those means unknown. JSON Schema separates all three; many client libraries collapse them, so the specification is where the difference has to be settled.
- Money and other exact decimals carried as strings. JSON numbers land in IEEE 754 doubles in most runtimes, which is how a total of 10.00 becomes 9.999999999999998 two systems downstream.
- Timestamps in RFC 3339 with an explicit offset, in UTC where possible. A local time with no offset is ambiguous once a year and nonexistent once a year in any zone that shifts.
- What a consumer should do with an enum value it has never seen. If new values can appear without a version bump, say so, and say what the fallback behavior is.
- Default and maximum page size, so the cap is found while reading the document rather than in production.
Choosing an Authentication Model
Authentication and authorization fail differently and should not return the same status. A 401 means the credential is missing, expired, or unreadable and the caller should obtain a new one; a 403 means the credential was understood and still does not permit this operation. Returning 403 for an expired token sends a consumer hunting for a permissions bug that does not exist. Credentials belong in the Authorization header rather than the query string, because URLs are written to access logs, proxy logs, and error trackers by default, and a secret that reaches a log is a secret you now have to rotate.
Every credential you issue is one you will eventually have to rotate, which is why the mechanism is much cheaper to choose at the start than to migrate later. Rotation is far less disruptive if the design accepts two valid credentials for the same consumer at once, so the consumer can cut over on its own schedule instead of yours.
- API keys. A shared secret in a header, fine for server to server where the key can live in a secrets manager. There is no expiry unless you build one, so revocation and rotation are entirely your responsibility.
- HTTP Basic over TLS. Simplest to implement, sends the credential on every request, and the password tends to end up in whatever the client library logs.
- OAuth 2.0 client credentials. Short-lived bearer tokens from a token endpoint, which caps the useful life of a leaked token and adds a hard dependency on the authorization server being reachable.
- Mutual TLS. The client presents a certificate, which is strong and hard to phish, but you are now running a certificate lifecycle, and an expired client certificate fails in a way that looks like a network fault rather than an auth fault.
- Signed requests. An HMAC over method, path, body, and timestamp survives being logged and resists replay when the timestamp window is actually checked. It is also the most work for a consumer to implement correctly, and canonicalization mistakes produce signature failures that are painful to debug from the server side.
- Anything running in a browser or a mobile app cannot keep a secret. A public client needs the authorization code flow with PKCE; a key compiled into a shipped app is a key you have published.
Rate Limiting, Quotas, and What the Caller Sees
Protecting the service and dividing it fairly produce different limits. A protective limit is shaped by capacity: a single global ceiling, tripped by whoever happens to be calling when it is reached, so a burst from one consumer throttles everyone else. A fairness limit is per key: each consumer gets a share, and a heavy but legitimate caller hits its own wall while the service still has headroom. Most APIs end up with both, and the per-key limit is the one that has to be legible to the caller.
A 429 with a Retry-After header is the difference between a client that backs off and a client that retries in a tight loop and turns a busy minute into an outage. Retry-After carries either a number of seconds or an HTTP date. Keep 429 distinct from 503: the first says you are calling too often, the second says the service itself is unwell, and a client that cannot tell them apart backs off from the wrong one.
A rate limit and a quota are not the same control. A rate limit refuses a request that would have been fine a second later; a quota refuses every request until the period rolls over. A consumer that exhausts a monthly quota on day nine should learn that from the error body, including when the window resets, rather than from a support thread.
- Fixed windows are cheap and allow twice the intended burst across a window boundary. A token bucket or sliding window costs shared state and behaves the way callers expect.
- The counter has to live somewhere shared. Per-instance counters behind an autoscaler mean the real limit is the configured number multiplied by however many instances happen to be running.
- Decide whether you are metering requests or work. One call that returns 10,000 rows is not one call's worth of load, and a pure request count rewards callers for always asking for the largest page allowed.
- Return limit, remaining, and reset in response headers so a well-behaved consumer can pace itself instead of finding the limit by tripping it.
- Budget health checks and token requests separately, or a consumer in a retry storm gets locked out of the endpoint it needs to recover.
- Log rejections per key. A limit that has never fired tells you nothing about whether it is set correctly, and one that fires constantly for a single consumer is usually a design conversation rather than an abuse problem.
Frequently Asked Questions
What makes something a REST API rather than just JSON over HTTP?
REST is a set of constraints, not a data format. Resources get their own URLs, requests are stateless, and the HTTP methods keep their defined meanings: GET is safe and cacheable, PUT and DELETE are idempotent, POST is neither. Plenty of working interfaces are JSON over HTTP with one POST endpoint and a verb in the body, and internally that is often fine, but it gives up HTTP caching, safe retries, and every generic tool that assumes method semantics. The distinction bites when a proxy, CDN, or client library between you and the consumer acts on those semantics whether or not you honored them.
What has to already exist before API work can start?
A system of record that actually holds the data, an environment to deploy into, and some existing notion of identity or accounts the API can authenticate against. You also need a named first consumer, even an internal one. If the underlying data model is still in flux, that is worth settling first, because an API makes internal ambiguity visible to everyone who calls it, and a field whose meaning depends on which team you ask will come back as bug reports rather than questions.
What does a REST API need to run on?
The same stack as the rest of the application, plus a few pieces that are specifically its own: a DNS name, a TLS certificate with automated renewal, and usually a reverse proxy or gateway in front of it. The gateway is where TLS termination, authentication, and rate limiting often live, which keeps that logic out of the application code and adds a component with its own timeouts and its own failure modes. Certificates from the free authorities are issued for 90 days, so renewal has to be automated and monitored rather than diarized. Throughput is more often bounded by the database connection pool than by the web tier, so pool size and the concurrency the API advertises have to be set together.
How does the work actually proceed?
Once the contract is settled and implementation is under way, the remaining stages are a staging deployment carrying data representative of production, credentials issued to the first consumer so it can run end to end against staging, and then the production cutover. Correlation IDs, structured logging, and monitoring have to be in place before the first real call rather than added after the first incident. Staging is where the unglamorous cases surface: expired credentials, records missing optional fields, and the consumer's own timeout being shorter than your slowest query.
What do we have to decide, and what do you need from us?
The decisions are yours: which system is the source of truth when two of them disagree about the same customer, what data may leave your network and under what retention rules, who holds the credentials, and who answers when a consumer reports a problem outside business hours. What is needed from your side is access to the systems involved, a non-production environment or enough sanitized data to build one, and somebody who can settle field-level semantics without a two-week loop. That last person is the one most often missing, and every question about what a field means queues up behind them.
Can you put an API in front of a vendor system we do not control?
Often, and the cost depends entirely on what the vendor exposes. A documented vendor API makes the work a translation layer with its own authentication and error mapping, inheriting whatever rate limits exist upstream. If the only access is the vendor's database, reads can be made to work, but the schema is not a supported interface and can change on any upgrade. If the only access is a file export, ask the vendor two things first: whether the export can produce changes since a timestamp rather than a full dump, and whether the schedule is configurable at all, since a nightly full extract you cannot reschedule sets a hard floor on how current the wrapper can ever be.