← Notes

Why Polymathy is on Actix-web, not Axum (yet)

We get asked this enough that it deserves its own note. Polymathy is written on actix-web 4. In 2026 the default-Rust-web-framework conversation has largely settled on axum for new projects, especially anywhere that touches tokio and tower. So the natural question is: why are you not on axum?

The honest answer is: because apistos, the OpenAPI macro layer we are using, is wired into actix-web, and the OpenAPI story is the only part of the framework we care about. This post is the longer version of that answer.

What we actually use the framework for

Polymathy’s surface area is small. There is one read endpoint — GET /v1/search?q=... — and a small constellation of documentation UIs hanging off it. The internal request flow does some parallel HTTP fan-out via reqwest, builds a HashMap, and serialises it back as JSON. That is the entire job.

We do not lean on the framework for routing complexity, middleware composition, or websockets. We do not have a streaming endpoint. We do not have sessions, CSRF, or templating. The handler is roughly twenty lines of business logic surrounded by serde derives. Whatever framework we picked, the framework’s job was basically: parse a query string, dispatch to one handler, and emit JSON.

What we do care about, intensely, is that the endpoint is described by an accurate OpenAPI spec, and that the spec is generated from the code rather than the other way around. We do not want to keep a openapi.yaml in sync by hand. We want the request and response types to come from the same Rust structs the handler actually uses, so that adding a field to the response struct shows up in /openapi.json automatically.

That single requirement turns out to dominate the framework choice.

The apistos integration

apistos is a macro crate that hooks into actix-web’s routing macros and produces a paths object that maps to your Rust types via schemars. The handler is annotated with #[api_operation], the request and response types derive ApiComponent alongside their Serialize/Deserialize, and the OpenAPI document is assembled at startup time. There is no separate IDL, no codegen step, no cargo run --bin gen-openapi.

When you boot Polymathy, the OpenAPI JSON is materialised once into a web::Data-style handle and served at /openapi.json. The four documentation UIs — Swagger, ReDoc, RapiDoc, Scalar — are each one route that serves a small HTML shell pointed at /openapi.json. Add a field to SearchQuery, restart, and all four UIs reflect the change. No npm step. No widdershins. No drift.

This is the part of the developer experience we did not want to give up. It is also, today, the part that is easier to do on actix-web than on axum.

The axum side of the same question

There are good OpenAPI-from-types crates in the axum ecosystem. aide and utoipa are the two most visible. We tried utoipa first, because it has the larger community and works across multiple HTTP frameworks. It works. It is genuinely fine.

The reason we did not switch is more boring than the reason we did not start there. We already had Polymathy on actix-web. The handler was working. The OpenAPI spec was correct. The four doc UIs were already wired up. The rewrite would have been a week of mechanical work — most of it untangling web::Data<T> to State<T>, swapping HttpResponse for axum::response::Json, redoing the error-mapping layer — to land in approximately the same place. There was no carrying capacity for that rewrite when we could ship the v0.2 release instead.

So we did not switch. We catalogued the migration cost in an ARCHITECTURE.md and moved on.

When the calculus flips

The honest version of this answer is that we expect to switch to axum eventually. The reasons are:

  1. tower middleware. The middleware story on axum is tower’s, and tower has the larger and better-maintained set of building blocks for the things we will need next: per-route timeouts, per-host rate limiting, load shedding under fan-out backpressure. We are going to want those when Polymathy starts persisting its USearch index across requests, which is on the roadmap.
  2. Library ergonomics for downstream consumers. A meaningful number of teams using Polymathy as a library — by depending on the crate and embedding the service inside a larger binary — would find axum’s Router composition easier to nest inside their own routing. Actix-web’s App is more total; it wants to own the runtime.
  3. The axum team’s investment in OpenAPI-adjacent tooling. utoipa and aide keep improving. The Scalar and RapiDoc integrations on the axum side have caught up. The gap that justified staying on actix-web in 2025 has narrowed.

The trigger for the switch will probably be the persistent-index work. When the handler stops being a stateless fan-out and starts owning a shared mutable USearch index across requests, the middleware story matters more, and tower makes that easier. At that point a rewrite is justified by the new capability, not by framework taste.

What we tell people building on top

If you are building a downstream service that calls Polymathy’s HTTP endpoint, the framework choice is invisible to you. The contract is the OpenAPI spec at /openapi.json and the response shape at /v1/search. Both of those will remain stable across a framework migration — that is the whole point of having them be generated from the types and pinned in the README.

If you are embedding Polymathy as a library (use polymathy::run), the framework choice does leak. The current run() function spins up an actix-web::HttpServer; a future version on axum would expose a Router you mount into your own server. We will signal that change with a major version bump and a migration note, not silently.

If you are contributing — please run cargo fmt and cargo clippy, as the README says. Beyond that, contributions that move the OpenAPI surface forward (more examples, better tag descriptions, richer error responses) are the highest-leverage thing you can do, regardless of which web framework the crate is on this quarter.

The smaller point

The reason this post exists at all is that “which Rust web framework” is one of those decisions that looks like it should be made early, in the abstract, on the basis of GitHub stars. In practice, the decision is made by the secondary ecosystem you depend on for the things the framework does not give you. For Polymathy, that ecosystem is the OpenAPI tooling, and for the last release it pointed at actix-web. For the next major release it will probably point at axum.

The cheaper way to say all of this is: Polymathy is on actix-web because apistos works, and apistos works because actix-web is what it was written for. When that calculus changes, the framework will change with it. We are not going to pretend we picked it on the merits of macro hygiene or async-trait ergonomics. We picked it because the OpenAPI story was good, and the OpenAPI story is the only story the binary cares about telling.

Boring is fine. Boring is, in fact, the point of a single-endpoint Rust web service.


Filed under Notes. Source on GitHub; docs at docs.skelfresearch.com/polymathy.