Having developed several successful APIs by the seat of the pants, I thought it time to put down on paper what features make an API successful, and what are the pitfalls of API design.
The following guidelines directly address the above stated goals for API design.
The right abstraction: It sounds axiomatic, but it is a common failure point. What's at stake is the API's ability to survive in the face of an upgraded implementation. Most APIs suffer from failing to anticipate this. The interface you provide must be sufficiently abstract to allow for a signifantly different implementation, or any code that uses the API will break as well. One source of this failure is an overly simplified API, which fails to generate any local state information which can accomodate multiple simultaneous uses of the API from the same process or machine.
Smallest possible surface area: This is the requirement to provide as simple an API with as few calls as possible, but not fewer. Excess surface area (too many methods) makes an API cumbersome.
Implementation-free deliverable: Of course you should also deliverable an implementation, but there must be a pure API component, such as a header file, which is totally devoid of any implementation information whatsoever. This is a common failing in both C/C++ APIs where headers typically include information about the size and nature of object variables. It doesn't help that these are protected or private, since they require recompiling the code that uses the API when these implementation details change. Use the Bridge Design Pattern to avoid this at all costs. API objects need contain only one piece of identifiable data, a pointer to an implementation class which is totally hidden from the API header. Java code, being intrinsically headerless, also suffers from this, but this can be fixed by following Java API design principles.
Intelligible User Model: Either an overly complex design or an overly cerebral abstraction will have a negative impact. Consider your user. The abstraction must still identifiably relate to the problem at hand, even though you anticipate applying this API to other related problems. The names of calls and parameters should be suggestive of their function. At all costs, avoid side effects, which--even documented--may mystify users.
Reentrant design: This is just not optional. You cannot guarantee that the same API will not be used concurrently on the same machine, user account, process, or even thread! An API will almost certainly need to change if reentrant design is missing from the first release, defeating the purpose of shipping an API in the first place. Each API should produce some sort of unique local state object, which can be updated if necessary by the API.
Clear documentation, including documented examples: Another obvious sounding observation, but many APIs provide one without the other.
Concise yet complete example: The example should be self-contained, provided with (electronic) source code, correspond to one of the documentation examples.
An API test suite, that will prove it works: There must be a shipping program that puts the API through its paces to establish whether it can be made to work in its current configuration. This is different from an example, whose purpose is to demonstrate the use of API. This suite must validate that the API is peforming as advertised.
Graceful failure: The API must recover from all real world failure conditions, such as network time outs, lack of disk space or write permissions, missing files, etc. It does not mean that it must survive every preposterous misapplication of the API. (See debug-mode version, below.) The nature of this recovery must be planned for each type of failure that can occur.
Clearly delineated, and reported failure conditions: Failure (and,--in debug mode--error) conditions must be reported accurately and categorically. Error codes are unintelligible to humans, but error strings are equally problematic for computers. Provide as a first line, a clear numeric error space. Then provide utilities for producing actual human readable messages, as well.
A shipping debug-mode version: Failures are due to real world conditions, and apply even when the API is called correctly. A debug version of the API should be shipped which also detects error conditions, which occur when the API is used incorrectly, or with bad data. The burden of this detection is usually not within the purview of a production API, since it may impede performance.
A bug reporting system: A good API captures enough information about its own failure conditions, that it can be reported. Now provide a well specified and easily followed method of reporting these conditions to the API vendor whenever the user feels it is the API that is in error.