"It's very easy to create a bad API and rather difficult to create a good one."
- Michi Henning, API Design Matters, Communications of the ACM Vol. 52 No. 5
First think of the consumer of the API. Think about his likely situation and the problems he's facing. He doesn't want to learn a new abstraction and he doesn't like the fact that your API is going to make him change the design of his program. Design for the benefit of the consumer, not your ease of implementation.
APIs don't merely implement algorithms that the consumer isn't expected to understand, they also present abstractions that reduce complexity. An FTP library doesn't give the consumer a power they didn't already have, it relieves the consumer of doing the socket programming themselves. If you're wrapping a lower-level API then please don't introduce new problems. Aim for designs that reduce complexity without contributing to it in other ways
Let the consumer give you instances of resource managers or event handlers to open auxiliary connections, write to log files, fetch lookup values, interact with the user and so-on
Don't force the consumer to create instances of a dozen different resource managers like logging classes, connection pools, name resolution classes etc. before they can call the first function. Raise events so that the consumer can handle only what they need in their circumstance
When events aren't sufficient for your IoC component, or your arguments require a strongly-typed value, then use Interface types--even if you expose concrete implementations as well, and even if you're certain that your concrete implementation is the only kind that will do. Assume your consumer knows what he's doing
Do you need IList, or just IEnumerable? Or do you actually need IBindingList and are trying to be too modest? If you specify too much you could force the consumer to work harder than necessary, and if you specify too little you risk being passed an insufficient implementation, or make it harder for the consumer to extend your API with a compatible wrapper
If your target platform has its own crypto primitives, graphics library, transactional memory, XML parser, or whatever--and the point of your API isn't to replace them--then don't reinvent and bind your API to your own implementations, even if you think they're better
Avoid returning data by setting global variables or writing to files, tables, sockets, etc. Let the consumer decide where the data goes and use aggregate data structures (classes, tuples) if it's necessary to return a success code along with the results
If an operation didn't work, or had an unavoidable (and un-IoC-able) side effect, then don't force the consumer to discover this by groping around referenced data structures or other environmental features. Return something other than
Did a connection fail because of a timeout, or because the remote host refused the connection, or because the address was invalid, or the login credentials invalid, or because a bird pooped on the switch, or what? Consider exposing and returning an Enum with the types of failures your API can detect
Whether by a built-in language feature or via overloads, chose sensible default values or behaviors (timeout values, for example) and give the consumer the option of providing the least you need
If you spot the line
If you ask for a filename as the first argument in one of your functions, then stick to that as a convention for any other function that needs a filename. If you arrange argument order randomly you'll force the consumer to memorize the signature for all of your functions, instead of being able to intuit them
Avoid creating black-boxes that the consumer has to poke and prod to life
Check what the consumer is passing to you and aim to fail before you begin using and modifying resources, especially if those resources don't support rollbacks
If you use cryptography or handle sensitive data, then have your product audited by a security professional. Use well tested, standardized crypto primitives that have been implemented by security professionals. Be careful to avoid buffer overflow vulnerabulities. Perform strict input validation. Do not invent your own ciphers, and do not implement crypto primitives on your own (unless that's the point), because even the implementation of trusted ciphers can be easily screwed up. Your consumer doesn't want an invisible hole ripped open in their software after they did everything right on their end
Identify a single concern for your API and stick to its scope. This concern could cover a broad category (like graphics, compression, networking), and if it does be sure to break it up into focused namespaces. You probably shouldn't bundle your compression library in the same binary and namespace as your FTP library
You may need to build more than one layer of abstraction to get up to the interface you'd like to present to the consumer. Networking now involves stacks 7 levels tall, for example. But keep each level separate, in the sense that each level could be re-used with a different "top" or a different "bottom" each coded to the right Interface
Some consumers will appreciate a wrapper that strongly types your
Following from above: even if the consumer passed an implicitly convertible string, always return the richest type that befits your output. If it's a date or time, return a
Give your types more utility for the consumer so they don't have to marshal values back and forth, just because they want a nice reactive UI or an easy way to save and restore data to disk
Libraries that link to the application's binary could be configured many ways: in code, as blocks in the application's own config file, or in a separate config file. The consumer will appreciate it if you offered at least the first two. When providing the option to configure the library through a shared config file, make sure to use namespaces or similar mechanism--such as .Net's ConfigurationSection class--to keep yours separate from the rest
Consumers don't need their Intellisense or other inspection tools cluttered up with all your helper classes and utility methods. Make
Exceptions add overhead to the consumer's code and aren't appropriate for reporting expected outcomes. Determine what kinds of errors should be expected for your kind of API and report them as return codes instead of exceptions. A file read/write error would be as common as black squares on a chessboard for a File I/O library, for example, but an out-of-memory fault is incidental to your API and should be thrown as an exception. Also raise exceptions for errors detected in the consumer's code (invalid arguments, unopened connections, out-of-bounds conditions, etc.)
Give the consumer the chance to factor exception handling into the design of their program before it's too late to change it
Release 1.0 of your documentation might not contain tutorials and How-Tos for everything your consumers use your API for, but listen to feedback and have a plan to improve the How-Tos chapter of your manual for subsequent releases
If your API is a wrapper for a low-level service, and you change the behavior (eg: setting a timeout to zero no longer means wait indefinitely), then document this clear and in bold for those consumers who already know or reference the documentation for the underlying service
They will serve both as a guide to the use of your API, and a framework to reproduce and report bugs that your consumers find in the field
For everything that can't be covered here, you will learn whether your API is actually practical and usable by employing it in a program that actually does something useful--not just a unit test or a proof-of-concept, but something that has a UI or interfaces with a real system. That program doesn't necessarily have to be a product that you sell and support, it can be an internal utility, too. Bonus points if the programmer doesn't even work on the API team and therefore provides a fresh perspective.
If you develop the API at the same time you develop the program, you will also have the chance to see and correct mistakes that add unnecessary complexity to the consumer's program, conflicts with the naming scheme of an existing API, or exposes an interface that's unintuitive and confusing. That may be better than all of the above tips combined.
1 - It's okay to use a product name as the root of the namespace.
Michi Henning's essay, referenced at the top, was the inspiration for this collection of tips. He himself was inspired to write it after experiencing the difficulties of using the socket Select() function in the .Net framework, and he passes on some excellent admonitions that bear repeating:
And since publishing, someone on Hacker News pointed out this article written by Trolltech, who designed the Qt GUI framework, and someone on Reddit pointed out the book Framework Design Guidelines, written by members of the .Net team at Microsoft.