Ultimo: a Rust web framework that generates the TypeScript client for you
Rust web framework with automatic TypeScript client generation
At a glance
- What is it?
- Ultimo is a Rust web framework on Hyper and Tokio that serves REST and JSON-RPC 2.0 from the same app, forbids unsafe code outright, and derives a typed TypeScript client from your Rust types with a code generator. Everything beyond the core is a Cargo feature, and the default feature set is empty.
- Who is it for?
- Ultimo suits a Rust team that serves a TypeScript frontend and wants the two ends to share one set of types, or a team that wants REST and JSON-RPC from a single binary with a documented shutdown path. It is the wrong choice for a service that exposes a schema first and generates clients from it, since Ultimo's model is type-first, and it is the wrong choice if you cannot accept an MSRV of Rust 1.86 or a framework that opts out of everything by default.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 5 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
REST and JSON-RPC 2.0 in one application
Ultimo is a modern Rust web framework built on Hyper and Tokio, with a stated goal of being type-safe end to end and secure by default. The framework is 100 percent safe Rust, with unsafe code forbidden at the crate level.
The structural choice that distinguishes it is that REST and JSON-RPC 2.0 live in the same application. Plain HTTP routes and RPC procedures sit side by side, and the RPC side supports batch requests and notifications. The repository carries separate example directories for the two styles, react-app-rest and react-app-rpc, and a react-app example that uses both, which is the clearest evidence of how the two coexist in practice.
Around that core there is a wide surface: WebSockets following RFC 6455 with a built-in pub/sub system and no extra dependencies, streaming responses through ctx.stream(...), typed server to client push with Server-Sent Events through ctx.sse(...), and authentication middleware for JWT, API keys and HTTP Basic auth, plus scope based authorization guards.
The audience is a Rust team that also has a TypeScript frontend, because the framework's differentiator is the client side. The homepage is ultimo.dev, the documentation lives at docs.ultimo.dev, and the repository also carries a CHANGELOG, a SECURITY.md and a BENCHMARKS.md.
TypeScript clients derived from Rust types
The headline feature is that you define the API in Rust and get a fully typed TypeScript client generated for you, with no hand written types and no drift.
The generation is a separate binary, ultimo-cli, and the documented invocation points it at your project and an output directory:
# Generate a TypeScript client from your RPC definitions
cargo run -p ultimo-cli -- generate --project ./backend --output ./clientWhat comes out is a client you call with types checked at compile time:
const user = await client.getUser({ id: 1 });
console.log(user.name);The mechanism underneath is the ts-rs crate, and the client-gen Cargo feature is what enables it. ts-rs derives TypeScript definitions from Rust types, so the client is a build output of your type definitions rather than a separate artifact that has to be kept in sync by hand.
That is a specific architectural stance, and it is worth naming the alternative. A schema-first pipeline, where the server publishes an OpenAPI document and a separate tool generates the client from it, has the advantage of an interoperable contract that non Rust consumers can use. Ultimo's approach is type-first, with the Rust types as the single source of truth, and the generation happening inside the Rust build rather than in a separate pipeline. The repository does contain OpenAPI examples, including an openapi-demo and a database-with-openapi, so both worlds are visible, but the client generation path is the Rust one.
For a team with a TypeScript frontend, that removes an entire category of bug. For a team with other consumers, it removes a contract they might have wanted.
An empty default feature set and an MSRV of 1.86
Ultimo's dependency model is unusual in a good way: everything beyond the core is opt-in, and the default feature set is empty.
[dependencies]
ultimo = "0.9"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }That short manifest is the point. A service that only wants JSON endpoints pulls in the core, and nothing else. A service that wants sessions, CSRF protection, static files, compression or a database driver turns on exactly those features and pays for exactly those dependencies.
The feature table lists the choices. websocket brings RFC 6455 support with pub/sub. session is cookie based session management. jwt is JWT authentication middleware using HS256. api-key is API key authentication with a pluggable store. csrf is CSRF protection using the double submit cookie pattern. static-files covers static file serving plus an SPA fallback through serve_static and serve_spa. compression is automatic gzip and brotli response compression, implemented in pure Rust with no C dependencies. client-gen derives the RPC client TypeScript types from Rust types through ts-rs. oidc verifies OIDC and JWKS tokens with RS256 or ES256, with Clerk, Auth0, Cognito and Supabase named. testing gives you the in-process TestClient with assertions and fixtures, and test-helpers covers WebSocket integration tests. There are per-backend SQLx features for PostgreSQL, MySQL and SQLite.
The minimum supported Rust version is 1.86, and the workspace manifest confirms it with rust-version 1.86.0 and edition 2021. That is a real constraint for a team on an older toolchain, and it is the kind of thing to check before designing around the framework rather than after.
A minimal handler and what it gives away
The quick start is short enough to read in one sitting, and it shows the shape of a handler.
use ultimo::prelude::*;
#[derive(Serialize, Deserialize)]
struct User {
id: u32,
name: String,
}
#[tokio::main]
async fn main() -> ultimo::Result<()> {
let mut app = Ultimo::new();
app.get("/users/:id", |ctx: Context| async move {The rest of the handler shows the error handling convention. A path parameter is pulled from the request, parsed, and a parse failure is turned into a typed framework error:
let id: u32 = ctx
.req
.param("id")?
.parse()
.map_err(|_| UltimoError::BadRequest("invalid id".into()))?;
ctx.json(User { id, name: format!("User {id}") }).await
});
println!("→ http://127.0.0.1:3000");
app.listen("127.0.0.1:3000").await
}The route is declared with a colon parameter, the handler receives a Context, and the response is written with ctx.json. Errors come back as a Result rather than as a panic, and the framework has its own error type with a BadRequest variant, which is what you would want from a framework that also serves JSON-RPC.
The security posture is not limited to the safe Rust claim, though that claim is real and enforced. The framework lists secure sessions and cookies, CSRF protection, a security headers middleware and request body size limits among its defaults, and its CI includes supply chain checks. The repository also carries a deny.toml, which is the cargo-deny configuration used for licence and advisory auditing of the dependency tree.
Shutdown that drains requests instead of dropping them
One feature deserves attention because it is the kind of detail that only shows up in production.
Ultimo has what it calls production ready shutdown. A request_timeout setting plus a listen_with_shutdown function drains in-flight requests when the process is asked to stop, and the documentation is specific that WebSocket connections are closed with status 1001 while this happens.
That closing code matters. 1001 is the going away status, which tells a client that the server is shutting down for a reason it can retry, rather than the abnormal closure codes that make clients treat a disconnect as an error. Deployments that terminate pods on a schedule will hit this path on every rollout, so getting it right is the difference between a clean deploy and a wave of client errors.
The request_timeout setting is the other half. A framework that can bound how long a handler runs is a framework that can survive a slow dependency instead of accumulating stuck connections.
The performance story is adjacent. The documentation claims native Rust on the Hyper and Tokio core, O(1) constant time routing, and benchmarks that are regression guarded in CI, with the details on a performance page in the documentation. The repository carries a BENCHMARKS.md and a benchmark example directory, and the Makefile has a bench target that runs criterion with the websocket and test-helpers features enabled. The repository's own claim is that the benchmarks are guarded against regression, which is the more useful property: it means a slowdown gets caught in CI rather than discovered in production.
Databases, testing and the example directory as documentation
Two integrations are called first class in the documentation: SQLx and Diesel, with PostgreSQL, MySQL and SQLite support through the SQLx features and an example directory for each of the two styles.
The testing story is similarly concrete. There is an in-process TestClient, response assertions and fixtures, behind the testing feature, plus test-helpers for WebSocket integration tests. An in-process client matters for a framework with a lot of middleware, because it lets a test exercise the whole stack without opening a socket.
The examples directory is the largest single piece of documentation in the repository. It contains basic, rpc-modes, openapi-demo, database-sqlx, database-diesel, database-api-styles, websocket-chat, websocket-chat-react, session-auth, jwt-auth, spa-demo, streaming and sse, and a REACT_EXAMPLES.md file tying the frontend examples together. Each is also a workspace member in the Cargo manifest, which means they are compiled by CI rather than left to rot.
The Makefile is the other place where the project's habits show. There are targets for build, test with cargo test --lib --no-fail-fast, a watch mode through cargo watch, a test summary script, benchmarks, coverage through a separate coverage-tool crate, clippy and formatting checks, and a ci target that chains check, test and coverage. The workspace also uses a moon configuration directory for task running, and the release process is driven by release-plz with a RELEASING.md document.
The dependency list is short and modern: tokio, hyper, hyper-util, http-body-util, serde, serde_json, validator, multer for multipart, ts-rs, tracing and tracing-subscriber for logs, and bytes, whose version is pinned to 1.11.1 with a comment noting it is a RUSTSEC fix for an integer overflow in BytesMut::reserve. A security patch recorded in the dependency pin is a small sign of attention to detail.
Version 0.9, two release tags, and a repository written for agents
The project is at version 0.9.4, and it publishes two tags per release, ultimo-v0.9.4 for the framework and ultimo-cli-v0.9.4 for the code generator, both on 2026-10-18, with the previous release ultimo-v0.9.3 on 2026-10-04. The last push to the repository was on 2026-10-18 as well.
Splitting the framework and the generator into separate tags is a sensible detail. The generator is a build time tool, the framework is a runtime dependency, and teams usually want to pin and upgrade them on different schedules.
The 0.9 version line says the API is still moving. For a framework, that is the usual signal that the shape of your handlers may change between minor versions, which is why the workspace keeps an examples directory that is compiled in CI: it is the tripwire that catches breaking changes before they reach a user.
The licence is MIT, and the repository includes a SECURITY.md with an unsafe-forbidden badge, consistent with the crate level forbid on unsafe code.
One more feature deserves a mention because it is unusual. The project advertises itself as built for coding agents, with typed Rust to TypeScript codegen, scaffolds that build, an llms.txt document for the documentation and an index on Context7. The repository also carries AGENTS.md, CLAUDE.md, a .agents directory and a .claude directory. That is a deliberate bet that a large share of the code in new Rust services will be written or edited by models, and that the framework should present itself in a form those tools can consume.
Editorial conclusion
Ultimo suits a Rust team that serves a TypeScript frontend and wants the two ends to share one set of types, or a team that wants REST and JSON-RPC from a single binary with a documented shutdown path. It is the wrong choice for a service that exposes a schema first and generates clients from it, since Ultimo's model is type-first, and it is the wrong choice if you cannot accept an MSRV of Rust 1.86 or a framework that opts out of everything by default. Verify three things before adopting it: that your TypeScript client generator runs as part of your build rather than by hand, that the JSON-RPC and REST surfaces you need are covered by the examples rather than by your own routing layer, and that the features you rely on, from jwt to sqlx-postgres, are ones you deliberately turn on and then pin.
Frequently asked questions
What is Ultimo, the Rust web framework?
It is a Rust web framework built on Hyper and Tokio that serves REST routes and JSON-RPC 2.0 procedures from the same application, with WebSockets, streaming responses, Server-Sent Events and built-in authentication middleware. The crate forbids unsafe code.
How do I generate a TypeScript client from my Ultimo API?
Run cargo run -p ultimo-cli -- generate --project ./backend --output ./client. The generation is driven by the ts-rs crate and the client-gen Cargo feature, so the client types come from your Rust types rather than from a separately maintained schema.
Which Rust version does Ultimo require?
The minimum supported Rust version is 1.86, with edition 2021 in the workspace manifest. The framework is published on crates.io as ultimo at version 0.9.4.
What does Ultimo include out of the box?
The default feature set is empty, so you opt in to what you need. The available features cover websocket, session, jwt, api-key, csrf, static-files, compression, client-gen, oidc, testing, test-helpers and the SQLx database integrations for PostgreSQL, MySQL and SQLite.
How does Ultimo handle shutdown and WebSocket connections?
A request_timeout setting combined with listen_with_shutdown drains in-flight requests on SIGTERM or Ctrl+C, and closes WebSocket connections with status 1001, the going away code that tells clients the shutdown is orderly.
How do I test an Ultimo application?
The testing feature provides an in-process TestClient along with response assertions and fixtures, and test-helpers covers WebSocket integration tests. The repository's examples include session-auth, jwt-auth, spa-demo, streaming and sse projects you can read as reference.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ultimo-rs-ultimo)Was this useful?