Documenting an API is one of the most boring things you can do as a developer. It's thankless work too, because any change in the code forces you to go back and update the docs so they don't go stale. There are tools that make it easier, sure, but it's still a pain.
Where the idea came from
A few years back, my team inherited an outsourced project. The main piece was an API that powered a specific part of an e-learning app, consumed by both the web client and the mobile app. The challenge was to understand what we had on our hands, the architecture, the business logic, the domain, and document it. Quite the challenge.
Documenting a legacy project with almost no existing information is hard work, and even harder a couple of years ago when coding agents didn't have the context windows they have now. After checking the market and seeing that every alternative still needed some minimum human input, I started wondering if there was a way to do this without all the effort.
Then it clicked. The documentation didn't exist, but the API was running, which meant someone knew how to use it. The client code that had been built up gradually knew which endpoints to call, which parameters to pass, and what responses to expect. That was where the real knowledge lived. We just needed a way to extract it, and the most natural approach was to observe the requests and responses going through the API and infer, from that data, how it actually worked.
It wasn't that simple, of course. Static analysis could generate a basic schema from types and payload shapes, but it fell short. The key was building something smarter that could also provide context and descriptions. A clear example: enum parameters. Take a field like role, which usually accepts a limited set of strings. The system needs to be smart enough to recognize that and tell it apart from a plain string.
The demo, the drawer, and paternity leave
I put together a presentation with some nice slides and a small proof of concept to share with the team. It seemed to land well, but you know how it goes: deadlines, priorities, other things showing up. The project ended up in a drawer. That API, as far as I know, is still undocumented today.
So I decided to keep working on it as a side project.
A few months ago, with paternity leave giving me those short unpredictable windows that are somehow perfect for thinking through half-finished ideas, I picked it back up for a proper relaunch with some improvements. One of the main things I wanted to solve was integration: how do you plug this into an existing API without asking anyone to rewrite their setup? I ended up with two options on the table: a proxy, which required no code changes at all, or a middleware, which required changing a single line. Since code is cheap now, I used a couple of those naps to write clear specs and built both.
For the middleware to work with real projects, it needed to integrate with the main frameworks, so after looking at the options I shipped adapters for Express, Fastify, Hono, NestJS, Next.js, h3, Elysia, and tRPC.
The integration had to stay boring. For Express, the goal was something close to this:
import express from "express";
import { easydocs } from "@easydocs/express";
const app = express();
app.use(easydocs({
project: "legacy-api",
storage: { type: "sqlite" },
}));
And for teams that cannot touch the API at all, the proxy path exists for that reason: put EasyDocs in front of the service, let traffic pass through, and build the documentation from what the API is already doing.
Making it efficient
One of the core requirements was that the system couldn't add any overhead or delay to request handling. The solution is a background queue that processes everything asynchronously and never blocks the actual request path.
Production APIs can also generate a lot of traffic, so I added a cache layer that hashes the response shape: if the structure hasn't changed since the last time that endpoint was analyzed, it doesn't get reprocessed.
Another useful improvement was moving as much as possible to static analysis and leaving only what's strictly necessary for the AI to handle. A threshold system that would sample only a percentage of incoming requests instead of all of them is still on the list — the reason it hasn't shipped yet is in the "Built During a Nap" section below.
Making it visible
The missing piece was obvious: where do you actually see the generated documentation?
In an early version I plugged in a Swagger-based library, but later I built my own dashboard because it gave me more control over how the data was presented and made it easier to add editing, export, and review features. Users can go through edge cases, fix naming, adjust auth schemes, and lock specific fields to prevent them from being overwritten automatically on the next run. For storage, the system supports SQLite or Postgres depending on the setup.
The moment I saw the system generate a coherent OpenAPI operation from real traffic, with useful descriptions and correct typing, was when it felt right to ship. It's not perfect, but the review-correct-lock cycle makes it worthwhile.
That cycle is the actual product for me:
- Real traffic creates or updates an operation.
- The dashboard shows the inferred params, body, response shape, and descriptions.
- You review the parts that need human judgment: names, auth, examples, edge cases.
- Anything you lock stays stable on the next automatic update.
And since stale docs are the whole enemy here, there's also a GitHub Action that diffs the spec on every PR and flags breaking changes, so drift gets caught before it ships.
On AI costs: you don't need a frontier model to get good results here, so it's pretty manageable. The project supports OpenAI, Anthropic, DeepSeek, and Ollama, so if you want to run it with a local model and pay nothing per request, that's a valid option too. Real traffic can carry sensitive data, so PII and secrets get detected and redacted before anything reaches a hosted provider; and for stricter setups there's an offline mode that pins everything to a local Ollama model, so nothing leaves your machine. And having a documented API justifies the cost many times over. This is one of those cases where AI genuinely earns its place: turning unstructured traffic into a structured schema is exactly the kind of work nobody wants to do by hand, but a model can handle it well.
Built During a Nap
I was writing specs for the efficiency improvements: response hashing and a ratio system that would only analyze a percentage of incoming requests. I handed both off to the agent, hashing to implement properly and the ratio as a proof of concept to see how it would behave. When I came back, the hashing was solid. The ratio, though, had a problem I hadn't thought through: it skips requests at random, which means you can miss calls with new schema information and waste cycles on ones that haven't changed. Not a good tradeoff. The hashing shipped; the ratio is still waiting for a better idea.
A small story
One morning while playing with Leo, I ended up with a small picture book showing different animals going about their animal lives. One of them was an otter, a small hardworking creature that collects things to organize and build its nest with whatever it finds. I thought it was a great match for easydocs and decided to use the otter as the library's mascot. GPT Images did the rest. It felt like the right kind of detail for a tool built in stolen naps.
If you want to try it, the code is on GitHub and the packages are on npm.

Top comments (0)