Tithe.ly Engineering

An API a Developer Can Love

by Shawn Pyle on June 11, 2024

Tithe.ly is built on numerous internal Application Programming Interfaces (APIs). These are the building blocks for our main application. Tithe.ly Integrations is primarily focused on getting Tithe.ly Giving data to external services. This requires my team to interface with internal and external APIs. We deal with both excellent and sub-par APIs. I’m going to share some of the things that make me love an API and why you should do it.

Documentation

An API is not complete without some sort of documentation to help direct developers where to start. The documentation should be clear, up to date, and include the following information:

  1. Authentication - How does a client authenticate with the API?
  2. Endpoints - A listing of paths that can be accessed in the API.
  3. Parameters - The inputs that can be sent to an endpoint.
  4. Response - What does a valid and invalid response look like?

The worst part of documentation is not if it’s incomplete, but if it’s wrong. If you provide documentation about the API, consider making it a requirement to keep it up to date when API changes are made. Even better, generate the API documentation from code or the API specification.

API Specifications

In a previous article, Specifying APIs for the Future, I go into API specifications in more detail, why they are useful, and how we used them in a recent project with success.

If you’re already creating an API specification, you can use it to document your API with tools like Swaggerhub or an alternative. Use of a VSCode extension like OpenAPI specification can help document it for shared repositories.

Schema

Consistency

The schema of an API response is the structure of the data. If you are writing a RESTful, CRUD (create, read, update, and delete) based application1, the schema should be consistent with the resources the API interacts with. If you do choose to use a different schema or non-standard parameters, consistency is the key. Each API has its own unique needs but the schema should be consistent within the API.

JavaScript Object Notation (JSON)

JSON is the common format for good reason, it’s easy to read without a lot of overhead. This should probably be the default format for any API you create. Changing of formats should be done via a Content-Type header.

For example, if you want to return a CSV file rather than a JSON object, have the client request it with a Content-Type: text/csv header. This allows you to provide the same data in different formats, should that be a necessary requirement with very little effort by the API client.

Use a data object in responses

Using a data object to wrap the main data returned in a response allows you to include metadata in the same response. Useful metadata is one area that sets apart excellent APIs. Useful metadata like the API version used, pagination, cursors, and errors make API requests easier to troubleshoot and use. In addition, using a data object provides extensibility to the response so that adding additional metadata doesn’t break the existing data schema.

Validation

If you’re allowing changes (create, update, delete) to the underlying data through the API, validation of the data is paramount. I, as a client of your API, do not know all the requirements of your systems. I’m hoping the developer who made the API knows what to allow and what to prevent when receiving data from me. In those cases where I get it wrong, providing details about what the issue is and how to solve it from within the response body eases the burden of solving problems that are likely already determined.

Documentation is an asset here, as long as it is current. You may even be able to use the API specification to validate data in your API (see node, python, ruby).

Error Handling

If you’ve ever started working with an API, then you likely started with errors. Since these are the first things a client developer will see, making it easier for them to understand what is not working and how to solve it will endear them to you. The response code should be appropriate to the error and documented when it deviates from the normal.

It is challenging (in RESTful APIs) when the response code doesn’t match what is in the response body. For example, a 200 OK response with an error message in the body is confusing. Again, consistency is very helpful.

// https://example.com/api/v1/articles/bad => 200 OK
{
    "error": "The requested resource was not found.",
    "data": null
}

Here is a simple example of what I love to see in APIs that handle errors well.

// https://example.com/api/v1/articles/bad => 404 Not Found
{
  "errors": [
    {
      "code": "NOT_FOUND",
      "message": "The requested resource was not found.",
      "detail": [
        "The article with the id 'bad' was not found.",
      ],
      "url": "https://example.com/api/docs"
    }
  ],
  "api": {
    "version": "1.0.0"
  }
}

// https://example.com/api/v1/articles/1 => 200 OK
{
  "data": {
    "id": 1,
    "title": "An API a Developer Can Love",
    "content": "..."
  },
  "api": {
    "version": "1.0.0"
  }
}

Security

Provide HTTPS. If you’re not using HTTPS, you’re not secure. If you’re not secure, you’re not trustworthy. If you’re not trustworthy, what are you?

Sensible Authentication

  1. Who will be using your API?
  2. How will it be used?
  3. How sensitive is the retrieved data?

Depending on the answers to these questions, the authentication and authorization requirements for your API will vary. I love it when an API uses the defacto standards for authentication (for example OAuth and JWTs) as I’m already familiar with them. However, if you need a different type, be sure to use it consistently and document it. (Are you seeing the pattern yet?).

Chunking

If the system has been around for a while, it’s going to accumulate data. Returning every record for a resource in a single request is a recipe for disaster or an outage. Chunking the data into smaller pieces allows the server to respond faster and not overwhelm the client handling the responses.

When chunking is used, providing pagination (page number, page size) or cursor (a pointer to the next set) allows the client to request the next chunk of data. This metadata should be provided in the response body.

Versioning

The debate rages on about how to version an API. All I know is that you should have one and it should work to prevent breaking the responses. I, for one, like semantic versioning as it communicates more about the scope of the change than calendar versioning does. At a glance, I can see if a release is likely to break my client or not.

Ideally, the version information would be accessible alongside the API documentation or even available at a /versions endpoint.


Me swooning over your API

Deprecations

This really only applies if you’re versioning your API, and you are versioning your API right? Deprecating features that I might rely on in advance of the removal is just plain kind. Oh, and document it.

Performance

The first priority is to make an API that works. I can suffer with slowish responses if the API is reliable. However, if the API is too slow, I’m not going to enjoy using it. Hopefully, you have the same goal to delight your customers and your API client developers. The faster the response, the better the experience.

Rate limiting is a good way to prevent abuses of your API that can cause real developer suffering. Be sure to provide the proper response codes (429 Too Many Requests) and headers (e.g. Retry-After).

Conclusion

APIs are immensely powerful tools for developers to build with. Do it right with awesome APIs and you’ll have developers who love your API.