- 5 min read
- People who have used your project before, and just need a refresher on API surface area
- People who are trying to figure out how to achieve specific desired behavior, are new to your project, and seeing if the fit is right.
Reasonable defaults
I like Python so much because the developer experience working is amazing - it’s an incredibly productive language because of the Zen of Python. It explicitly calls out: there should be one— and preferably only one —obvious way to do it..
It means Python library and modules often have one, clear, canonical way to do things and that implementation will typically have a 5-10 line sample that will cover user needs 99% of the time. It will try to handle all the ‘gotchas’ for you in a reasonable manner, and only if you want to customize it do you need to check the API docs. It doesn’t preclude having different ways of doing things, but there should be one obvious way.
Building blocks
Technical documentation sets will typically show you documentation for class API surface areas, maybe auto-generated from source comments. That’s great! Except… it’s probably not that helpful to the people who need it most. Developers are not trying to problem-solve how to use your class’ API, they’re problem-solving for scenarios. They are after desired behavior, and your class’ API surface area is the afterthought.
A big gripe I have with documentation - in particular that of C#/.NET - is how often it falls into the trap of documenting the usage of specific methods or class basic building blocks without documenting the broader interactions, resulting in a myopic view of the overall desired behavior a developer is after.
It leaves users to stumble through assembling pieces to solve their scenario the hard way - akin to if a LEGO instruction booklet listed only all the different ways in which each LEGO shape could be used, but contained no step-by-step assembly instructions.
Knowing your audience
Writing quality documentation is about knowing your audience. Readers of documentation will generally fall into two buckets:
Very specific API documentation best helps the former, while good examples and detailed remarks help the latter.
API documentation does little to help the latter group. It assumes they know about how the parts of your project are supposed to interact, within itself or with the language’s standard library. That’s a great way to have users to shoot themselves in the foot, instead of guiding users the right direction.
For example, if you are deeply experienced in the C#/.NET ecosystem and patterns, you probably already have an idea of what classes need to interact together to achieve the desired behavior and the patterns necessary to avoid gotchas down the line - it’s extensive and wonderful, in-depth API docs are perfect for you!
But if you are a developer reading the docs with a fresh eye, trying to figure out ”how do I issue a HTTP GET in C#?” or ”how to I verify a self-signed X.509 certificate with .NET?”, you won’t have as good luck. You’ll probably end up implementing HttpClient
in the obvious but sub-optimal way that causes socket exhaustion, or be lulled into a false sense of security from X509Chain.Build()
without realizing nuance in the .NET implementation details warrants additional verification on top of the X.509 class methods.
Example - Polly and REST APIs
Say you want to make REST API calls to an external dependency, and want to also use Polly to add resiliency those calls. Luckily, there’s a whole docs page for that! It’ll even (briefly) touch upon that socket exhaustion gotcha with HttpClient
.
It shows building blocks - how to add a policy to a HttpClient
instance, and how to select policies.
However, the first real-world problem a user is going to run into is going to be needing multiple policies. Different endpoints are going to have different needs, and most importantly: POST is not idempotent, so applying retries are going to wreak havoc when the developer encounters their first failed POST call.
After hitting that, a user may realize their error and search along the lines of ‘polly httpclient (idempotent OR idempotency)’ on the upstream docs or Microsoft docs and promptly come up with no results.
Broaden the search results to any website and the top result is a helpful post by Scott Hanselman that mentions this specific issue:
GOTCHAS
A few things to remember. If you are POSTing to an endpoint and applying retries, you want that operation to be idempotent.
Since official docs are all focused on tying policies to HttpClient
and subsequently how to inject it with DI, what’s clear is that policies were intended to be tied a HttpClient
instance - so the obvious question would be then how do I consume multiple HttpClient references via DI so I can apply different policies?
And just like that, the docs led the user down an obvious path, but the wrong one that shoots them in the foot: you shouldn’t try and inject multiple HttpClient
instances.
Let’s go back to our idempotency search and pick results further down the page: the blog posts by twilio and no dogma, both talking about this gotcha and demonstrating how to inject a single HttpClient
instance and vary policies based on the HttpRequestMessage
properties (i.e. REST method) with AddPolicyHandler()
or AddPolicyHandlerFromRegistry()
.
In short
Remember the Zen of Python?
There should be one— and preferably only one —obvious way to do it.
Documentation should make the recommended implementation, while avoiding gotchas, obvious. Include a remark about idempotency in sections that apply retry policy to HttpClient
. Include a code sample that shows explicitly how to tie multiple policies to a single HttpClient
, and also the recommended way to inject it with IHttpClientFactory
.
What would amount to under 50 lines of sample code could show developers with fresh eyes a canonical implementation working around issues 99% would likely face, and save users hours of debugging. That’s good documentation.