Libraries are cohesive pieces of reusable code that encapsulate some functionality or business logic and exposes it to developers through an API, which a collection of public functions, classes or interfaces. There are many aspects to successful library design, but the API is crucial.
Conceptual Model First
The most important principle is to have a coherent conceptual model of what your library does. Let's consider, as an example, a library that allows you to interact with a SQL data store. There are some very common workflows in this domain. You need to connect to the data store, you may need to configure it in some way (e.g. create tables) and you need to perform the famous CRUD operations (create, read, update, delete). There are myriad ways to structure these operations in an API. Once, you clarified for yourself how you want to model the operations and workflows the library supports, you can start designing your API. For example, a relational database engine can support multiple separate databases. Suppose you decide to go with an object-oriented API and have a DBEngine object. Now, do you allow performing operations on different databases directly from the DBEngine object, or do you add a separate Database object that you can get from the DBEngine?
When coming up with the conceptual model it often helps to keep a 1-to-1 mapping to the domain. Another good way is to look at how other successful libraries in the domain model it. For example, SQLAlchemy is a very successful Python library for working with relational databases and it has a very explicit conceptual model that deals with many intricacies of various SQL database engines.
Keep the API Simple (you can go as fancy as you like in the implementation)
A simple API is critical to adoption. If the API is difficult to use, your users — developers — will avoid it. Even if you have a captive audience and the developers are required by company policy, or any other reason, to use your API, you should still keep it simple to avoid frustration and a lot of time-wasting requests for help. What does it mean to keep it simple?
Make it easy to accomplish tasks with as few calls as possible
If in order to query a database you have to connect to it, initialize it, verify it is ready, prepare a query, run a query, extract the result and parse the result, then you have made your user work very hard and left a lot of room to make mistakes. Try to keep it tight. Good defaults go a long way. You can also provide wrapper or convenience functions layered on top of an advanced low-level API with lots of options.
Try to avoid order dependency
If you must perform operations in a certain order, it is easy for the users to forget a step. It is better to ensure that you can't even call a function before a required previous step has been completed. For example, if you need to connect to a database before you interact with it, don't provide a way to create a disconnected Database that you must call connect() on and only then you can query it. Instead provide a connect() method that takes the connection parameters and returns a connected Database object that you can interact with immediately.
Use Idiomatic Style
Idiomatic style means providing APIs that use language features and best practices to perform common tasks. For example, in Python it is considered idiomatic to provide direct attribute access (or properties) directly on objects:
Where in other languages such as Java or C++ it is common to use getters and setters:
Iterating over collections often has direct language support:
- for item in collection (Python)
- enumerators in C#
- iterators in C++
Your users will be very happy if your API uses proper idioms they are familiar with and they'll save you design time and the risk of getting it wrong or just alienating your users with yet another proprietary way to perform a common task. There is plenty of room for creativity when designing APIs. Don't re-invent the idiomatic wheel
Sometimes you need to expose a service or an internal functionality as client libraries in many programming languages to reach as many third party developers as possible. In this scenario, it may be acceptable to ignore the advice of the previous section about idiomatic style for each language (especially if you don't have a lot of expertise in every language) and aim for a lower common denominator of a simple API for all languages. It is a judgement call you'll have to make in the trenches.
It is very common to develop high performance core functionality in C due to its speed and direct access to the underlying machine. The C ABI (Application Binary Interface) is also universal and pretty much any language worth its salt can interface to a C library. It is then very common to create multiple language bindings to a C library and make the functionality available in other languages. There are tools such as Swig that create automatic bindings in a plethora of languages for C and even C++ code (notoriously difficult). Many programming languages come with their own way to bind to C libraries.
Can you Generate your API?
Often, you can impose some discipline on your outer-facing code — including naming conventions and the data types you use in your function/method signatures. In these cases, it is possible to generate a web API or other APIs automatically. These is great, because you can make changes to your code, and as long as you adhere to the constraints, your API code will always stay in sync.
Know your Users
It is important to know your target audience. If you're in a C++ shop, writing libraries for your fellow engineers, it may be fine to just provide a bunch of classes and templates and take advantage of the latest C++14 syntax. But, if you need to expose your service to the general population, you had better wrap your C++ code in a simple C API and/or provide a slew of other language bindings.
Useful Error Messages
Error handling and error messages are key to good APIs. Your API is measured when something goes wrong. Aim to provide information and error messages at the right abstraction level for a user who doesn't know how the internals of your library work. You should also consider providing internal error information such as deep stack traces. Often, you or your peers will debug your own APIs and you'll want to see what's going on inside.