Skip to content

Instantly share code, notes, and snippets.

@MCJack123
Created June 6, 2022 04:13
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MCJack123/39ac0847579b3676cc098aca5860c758 to your computer and use it in GitHub Desktop.
Save MCJack123/39ac0847579b3676cc098aca5860c758 to your computer and use it in GitHub Desktop.
On Writing a Sane API

On Writing a Sane API

Over my years on the ComputerCraft Discord server, I've had the opportunity to witness the creation of numerous APIs/libraries of all sorts. I've gotten to examine these APIs in depth, as well as answer questions involving the APIs that the creators or users have. As an API designer myself, I compare the designs of other APIs with my designs, and I've noticed a number of patterns that make or break an API design. I've seen plenty of designs that make me go "WTF???", and lots that I just can't understand, even at my advanced level of programming (not to toot my own horn).

This article outlines some rules for making a sane API, which is easy to use, understandable, and doesn't make the user spin in circles to make things with it. Note that when I use the term "API", I'm primarily referring to code libraries and their public interfaces, but a number of points can be applied to web APIs as well. Since I have the most experience in Lua APIs, I'll be focusing on Lua APIs, but this can also be applied to any other language.

Keep It Consistent

Consistency is key in any sane API design. By keeping things the same across the entire API, you reduce the number of surprises the developer will find when they try to use it. This applies for every part of the API, including, but not limited to:

  • Name formatting (including case, general length, and word choice)
  • Types of arguments and return values (don't use numbers for some functions, and string-numbers for others)
  • Order of arguments (if you put X/Y as the 2nd/3rd argument in one function, do that in all functions)
  • Property names (don't have one type use width/height and another use w/h)
  • Error message format (use similar word choice and placement for all errors)
  • Division of submodules (don't place some similar functions in one submodule, but others in the root)

Before writing the API, decide on some standards that you'll follow throughout the API. Then follow this standard in every public interface you write, and don't deviate from the standard unless absolutely required (e.g. you also implement functions for a different API than yours). Having the standard written down somewhere can be useful not only for yourself, but also for the developer. Even better, develop standards that you'll use in all your APIs. For example, I usually use names that are as few words as possible while still getting the idea across, with camelCase to separate words. If you have a personal standard, people who know how to use one API of yours won't have much trouble using another.

Make It Concise

There's no need to make your function/variable names overly descriptive. The documentation is where you can be highly descriptive about the function, but the names should be just long enough to describe what they do. A function or property should ideally be no more than four words long; a type name should ideally have no more than six words. Instead of calling your function drawBoxOnScreenWithSizeAndColor(x, y, w, h, c), use drawBox(x, y, width, height, color) - most IDEs will display the parameter names when typing or hovering over the function, making it unnecessary to describe the parameters in the type name. (Note that this does not necessarily apply to Objective-C, which typically uses descriptive message and parameter label names.) Using shorter names will make your code easier to read and look at, and also makes it easier to type, especially if the developer's working in an editor without autocomplete.

In addition, keep the number of arguments to functions as low as possible. Don't give your functions 13 optional arguments just to make it possible to construct an object in one call, or to specify different options in the process. If you're making a constructor, only use arguments that are absolutely required to create the object, and have the developer set properties or call setter methods for any additional options. For single-call functions that have a lot of options, consider having the developer pass a table/dictionary/object of options instead of positional arguments. This has the benefit of explicitly defining which arguments correspond to which option, and allows complete omission of any default arguments, as well as not requiring the developer to remember the order of arguments. Python has the ability to use named arguments, so prefer using named arguments when there are a large number of options. Lua even has shorthand for passing a single table argument, allowing named calls with a similar syntax to Python:

local function lotsOfArguments(options)
    if options.test then
        print(options.myArgument)
    end
    -- etc.
end
lotsOfArguments{test = true, myArgument = "Hello World!"}

Finally, instead of having large metafunctions that do everything, consider providing individual functions that just do one thing. This allows developers to combine your functions as they need without having to follow the same mindset you may have while writing the API. A function should have one concise purpose, and no more - if more is needed, the developer can combine functions to make it.

Maintain Modularity

In many languages, importable libraries are called modules. This is because the libraries are made to be modular, and able to be loaded and unloaded cleanly as one unit. When writing a library (especially in module-based languages), you should maintain this idea of modularity. The biggest rule to follow is to use as little global state as possible, especially environment-level globals. This means that you should not use any global or module-level variables to store state about the API. Instead, opt to use handles or state objects to store all state. Do not store things in the global environment of the caller (Lua _ENV, JS window, etc.). This can lead to very bad things happening when the "module" (it is no longer modular) is reloaded, plus possible conflicts with other "module"s. By not using global variables, you reduce the number of surprises that may happen due to side effects of function calls. This is especially important if you intend to allow the developer to use multiple copies of some object (e.g. Lua 5 allowing multiple parallel states (VM instances) in the same program), or you want to make your library multithreading-capable (global state leads to race conditions).

Another good way to modularize your code (as well as just general code cleanliness & organization) is to split similar functions into a submodule. This can be a separate file that you have to load manually (good for large/complex submodules), or just a simple namespace or table inside the main module (good for single-file modules). One example of this is in my AUKit library: I have the root aukit module store the loader functions + miscellaneous functions; an aukit.effects submodule (in the same file) holds functions that modify loaded audio in-place; and an aukit.stream submodule provides a number of streaming functions that all return a loader callback for use in aukit.play(). This goes along with the natural human tendency to categorize similar things, and will help the API make more sense, as well as keeping your code naturally organized.

Be Fool-Resistant

When writing libraries, I find that a lot of people tend to make their APIs understandable for them. This is okay if you're just making it for yourself, but it can become a problem when you want other people to start using the library. Just because you know what something means doesn't mean that everyone else who uses your API will know what it means too. A sane API should be clear to anyone with a base-level knowledge of the purpose of the library; e.g. an audio manipulation library should be clear to someone with a base level of audio editing. In addition, assume that they will do everything wrong - this helps avoid creating unintelligible errors and catastrophic failures when someone does do something wrong.

When you write functions that are public-facing, act like the user is a monkey with a typewriter, who only knows how to press keys to make things work. In reality, a lot of newer programmers are like this in a way, copying code they find online to make things work. This is good for learning, but once the programmer starts adjusting things to fit their needs better, they often make mistakes that even a high-level programmer wouldn't expect. Again, this is fine for the learner, but it is not good for your code. At the very least, check all inputs your code takes, especially in a dynamically typed language like Lua or JavaScript. Never assume the inputs are correct - in fact, assume the inputs are wrong in every single way possible before you start operating on it. This also gives the developer an opportunity to get an error message that actually describes what's wrong - getting an error of program.lua:15: bad argument #1 to 'loadFile' (expected string, got number) is much more understandable to the developer than api.lua:893: attempt to index a number value. You should always prefer to bail cleanly if something goes wrong - be ready for things to fail, and handle them as gracefully as possible with as descriptive of errors as possible (even if this means triggering a panic - just make sure there are no side-effects).

In addition, no matter how good your documentation is, you should assume that the user did not read it. Many times, people will jump into writing code just using autocompletion, rather than looking at the actual documentation of the functions (myself included). This doesn't make it okay to write crappy docs (more on this below), but you should make your API at least decently navigable without reading the docs. This not only helps the lazy be lazy, but it also contributes to keeping the code clean, as it forces you to write more understandable names as described above. The functions should be able to describe the docs nearly as well as the docs describe the function - this means that there should be no surprises when reading the docs for a function by name.

Write Good Documentation

The key to getting people to use your code is to document everything well. If it is not documented, or the docs are hard to read, people will avoid your code even if it is the best choice available. It's hard to use code if you don't know how to use it, and this gets worse as the complexity of the library (and thus the likelihood that a library is better than implementing your own) increases. A sane API extends beyond just having clean code - they also have clean documentation to help the developer as much as possible.

The first part in writing good docs is to describe everything properly. Proper function docs should contain:

  1. A brief, one-line overview of the function
  2. Further description of the function, including any special notes (if necessary)
  3. Descriptions of each argument's purpose with types
  4. An explanation of the return value
  5. Examples of how to use the function (if desired, recommended if the function's complex)
  6. Links to any related functions

This procedure should be followed for every public function in your API. Failure to document a function will result in decreased usage of that function. All documentation should be written in proper grammatically-correct English (or whatever language you choose - English is pretty standard, as is Chinese) as much as possible - use tools like Grammarly to check your docs for proper grammar (you may ignore warnings about using programming words). Native English speakers may find poor grammar in documentation to indicate poor code quality as well, even though a large number of developers speak English as a second language, so ensuring your docs are written well will help native speakers understand it better. Also, be specific about what you're writing about - try not to use pronouns (that, it, etc.) unless it negatively affects readability.

It's also a good idea to include module-level documentation to describe the API as a whole. This is where you write a multi-paragraph description of what it is, how it works, potential use cases, and full-formed examples of how to use the API. These help describe the API as a whole, and gives some more insight into usage beyond single functions. You can also declare the license statement here, as well as author and version.

To assist in writing documentation, I recommend you use a documentation generator such as Javadoc, Doxygen or LDoc. These tools allow you to type your docs directly in the code using comments. This means you don't have to jump to a different file or workspace to document your work, and you can keep everything in one file. This also allows people to read the docs directly in their code editor - in fact, many IDEs include automatic parsing of select doc syntaxes, showing the descriptions in the editor while writing code. Finally, the tool will automatically handle all styling of the resulting web page for you, so you don't need to manage styling or links at all (unless you want to make it look better - standard styles are typically very basic, but usable). To generate a webpage version, all you need to do is (in some tools) create a default config file and pass in the source file(s) you want to generate for, and it'll spit out HTML and CSS with the documentation extracted and formatted, suitable for uploading to places like GitHub Pages.

Writing documentation won't be helpful unless there's a place where developers can easily access it. If you have comment docs, this is a great first step, but it's also helpful to be able to bring up just the docs separately from the code (especially if the source is long). The easiest way to do this is to generate the docs as stated above, upload the docs in a /docs folder in a GitHub repo (preferably the same one with the code), and enable GitHub Pages on the repo (in Settings => Pages, then choose the source branch and path). This will make the docs automatically available at https://your-username.github.io/your-repository/. Once you do this, add a link to the docs to README.md (you have one, right?) and the repo URL field so it's easy to find them. (Note that making docs available as a webpage isn't strictly required - especially if you use Gists/single-file distribution methods - but it's a very helpful resource when deep in using the API.)

An Example

My favorite example of what I consider a sane API is my AUKit library (which I've referenced before). I used all of the concepts I've described when designing it:

  • I kept the functions consistent: all names are single words, all Audio methods return new values while all aukit.effects functions modify in-place
  • I kept it as terse as possible while still making sense: the single-word function names describe what they do, functions (mostly) do exactly one thing (and I should probably adjust a few functions to take named arguments as well)
  • I made it modular: functions with similar purposes are stored in separate subtables, no globals are used besides a defaultInterpolation value in the module table (which can be overridden locally), functions do not produce side effects
  • I made it fool-resistant: all arguments are checked before use, malformed files trigger a readable error
  • I wrote good documentation: all functions, methods and fields are fully documented both in inline comments as well as online through GitHub Pages

If you want to see how to follow this guide, you may examine AUKit's code and documentation page - I recommend it as a template of how good APIs look. Of course, this is my own project, so I can't give a perspective from someone else's view, but I'm personally quite proud of it.

Conclusion

Sane APIs are pretty much a necessity if you want to make a successful library. Consistency and conciseness is key when writing a public interface - not following these will cause confusion, making people turn away. Keeping the module modular means your library is easy to simply snap into a program without worrying about conflicts, as well as keeping your own code structure clean. Protecting your code from foolish inputs will make sure that you don't inadvertently cause catastrophe, and that the developer can get helpful feedback on their errors. Finally, writing good documentation for your code is one of the most important things when releasing the API for use by other developers. By following these guidelines, you'll be able to make a public interface for your code that people can look at and think, "That is a very sane API."

@9551-Dev
Copy link

Im going insane

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment