A Guide to Rust ORMs in 2025

Joshua Mo  •  (Updated on )
Cover image

In this article, we're going to talk about Rust ORMs and compare the most popular Rust ORMs that you can use today in your applications. We'll also explore whether you should use an ORM at all, and when raw SQL might be a better choice.

What is an ORM?

A Relational Object Mapper (ORM for short) is a piece of software that aims to solve the issue of using SQL directly by letting you map objects in your code to SQL. For example, you may have an SQL query that looks like this:

SELECT FROM CAKES WHERE NAME = 'Test Cake';

This may be written as this:

let name = "Test Cake";
let cake: cake::Model = User::find()
    .filter(cake::Column::Name.contains(name))
    .all(db_connection)
    .await?;

Although initially there is more boilerplate setup than using a raw SQL library might use, in the long run it can save a lot of developer headaches when getting SQL queries to work - it also makes onboarding developers who are new to a codebase much easier. Additionally, you get the benefits of any IDE plugins you wish to use - for example, LSP (Language Server Protocol) plugins and Intellisense.

SeaORM

SeaORM website homepageSeaORM website homepage

What is SeaORM?

SeaORM is a fully async-friendly Rust ORM that aims to "help you build web services in Rust with the familiarity of dynamic languages". This library builds on SQLx and abstracts the raw SQL away to provide a clean interface that allows you to use structs as models, using derive macros and traits to allow you to build the experience that you want. It also comes with a CLI for generating migrations, entities, and models.

SeaORM also implements a system called ActiveModel through traits to be able to extend the behavior of models that an application might use. Additionally, you can add traits for extending behavior before or after saving a record, and the ActiveModel itself. This is quite helpful for us as it allows us to slim down the application code while abstracting it away to other areas. A new framework called Loco aims to reproduce the "Ruby on Rails" experience in Rust by including heavy use of SeaORM to slim down application code by allowing you to instead use traits to implement the behavior that you want - you can explore this in our guide to using Loco with Rust.

SeaORM is plug-and-play and intuitive for basic use cases, with fast compilation times compared to Diesel. It auto-generates structs from your schema and works well with frameworks like Axum.

SeaORM has quite a lot of helpful documentation on the SeaQL website. There's a page for mostly everything you can do with SeaORM. Some parts like ActiveModel that are quite useful to know about are mainly tucked away into parts of other pages, so it could be inferred there's an assumption that you're going to read every page or use the search bar. If you plan to use SeaORM regularly it would be a good idea to do so already, but this can make casual browsing somewhat more awkward.

If you have a lot of different models or tables that you need to use, SeaORM is very helpful. If you have a lot of different things you need to keep track of and have a Rust LSP plugin or intellisense installed, it's easy to ensure that all of the SQL database interactions "just work" without needing to debug anything! This solves a particularly large issue for teams with members who may need to interact with the database but are not skilled in SQL.

One thing that you might find to be a hindrance is knowing where to import your dependencies from. Particularly if you're using multiple models in one file, it can be annoying to rename everything! It can also be somewhat complicated to implement your own ActiveModel behavior. If you're a less experienced developer, this can lead to some headaches. The set-up time may also be a turn-off particularly if you have a lot of tables to set up due to how much method chaining there is.

Unlike Diesel, SeaORM doesn't provide compile-time type checking - you'll need to rely on runtime integration tests to catch database-related bugs. However, SeaORM does offer flexibility with escape hatches for raw SQL when you need it, and it's particularly good for dynamic queries.

Additionally, there are a couple of initial bumps that a newer developer may come across while using it - particularly, the need for a CLI and looking at what the migrations do exactly. Additionally, although you can migrate SQL files directly to SeaORM migrations, the generated migration files themselves are extremely long. This is a migration that adds one table with one column:

// src/migrator/m20220602_000001_create_bakery_table.rs (create new file)

use sea_orm_migration::prelude::*;

pub struct Migration;

impl MigrationName for Migration {
    fn name(&self) -> &str {
        "m_20220602_000001_create_bakery_table"
    }
}

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    // Define how to apply this migration: Create the Bakery table.
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_table(
                Table::create()
                    .table(Bakery::Table)
                    .col(
                        ColumnDef::new(Bakery::Id)
                            .integer()
                            .not_null()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(Bakery::Name).string().not_null())
                    .col(ColumnDef::new(Bakery::ProfitMargin).double().not_null())
                    .to_owned(),
            )
            .await
    }

    // Define how to rollback this migration: Drop the Bakery table.
    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .drop_table(Table::drop().table(Bakery::Table).to_owned())
            .await
    }
}

#[derive(Iden)]
pub enum Bakery {
    Table,
    Id,
    Name,
    ProfitMargin,
}

As you can see, it's pretty long. Additionally, the Iden derive macro is not clearly explained to the user in the documentation. Despite this, it is crucial to be able to implement the definitions for the migration itself.

In terms of performance, it is slower than other ORM crates (namely, Diesel) - you can find detailed performance metrics in the Diesel repository. While SeaORM is a crate that can offer a lot of functionality, you may have to sacrifice some performance in exchange for it. Diesel also produces smaller binaries and has better overall performance characteristics.

Using SeaORM with Shuttle

By default, Shuttle provides a SQLx connection from our shared_db crate which you can turn into a SeaORM connection:

#[shuttle_runtime::main]
async fn axum(
    #[shuttle_shared_db::Postgres] pool: PgPool,
) -> shuttle_axum::ShuttleAxum {
    let conn = SqlxPostgresConnector::from_sqlx_postgres_pool(pool);  // pg conn
...
    let app = Router::new()
        .route("/", get(some_route))
        .with_state(Arc::new(conn));

    Ok(app.into())
}

In production, the macro will automatically allow the Shuttle servers to provision a Postgres instance to you with no setup required!

Diesel

Diesel website homepageDiesel website homepage

What is Diesel?

Diesel is the "other big choice" that you might consider when wanting to use an ORM in Rust. It can be more accurately described as a data mapper and query builder. However, because it offers many features that an ORM normally might (compile-time checking, migrations, mapping structs to database objects) it is considered functionally the same as an ORM. Diesel is used at scale in production - notably powering crates.io via diesel-async.

Compared to SQLx, the table setup is much cleaner:

diesel::table! {
    users {
        id -> Integer,
        name -> VarChar,
        favorite_color -> Nullable<VarChar>,
    }
}

Instead of mapping directly to Rust types, Diesel maps SQL types as Rust unit structs. However, when you're writing a struct that you may want to use for querying the database, note that the documentation says you shouldn't directly use these types in your structs.

Instead of being required to use models or entities directly, you can use Diesel's methods to do simple inserts, updates, or selects instead:

let new_user = (id.eq(1), name.eq("Sean"));
let rows_inserted = diesel::insert_into(users)
    .values(&new_user)
    .execute(connection);

The combination of both of these makes for a much more simple interface to work with than SeaORM. There is no specific interface for extending your models, but you can also add a manual implementation.

One of Diesel's main strengths is that it enforces compile-time safety by checking the queries from the table! macros. This is a huge advantage for developers who want to make sure that their queries work and it means you won't get runtime errors trying to run SQL queries. It is also particularly relevant if you're running a lot of large queries where you have a lot of things going on. Diesel targets flexibility, database-specific extensions, and type safety - it explicitly doesn't try to hide database differences for the sake of portability.

If you're looking to use a web service with Diesel, it should be noted that Diesel is primarily synchronous and uses native drivers (the primary reason behind native async incompatibility). When using Diesel, you're using a highly-optimised implementation of the transport protocol of the database library. However, if you want to use a pure Rust stack, Diesel may not be for you. With regards to enabling async, there has been ongoing discussion in the Diesel GitHub issues. If you'd like to use Diesel in an async context idiomatically you can always use diesel-async or diesel-deadpool (or one of the many other crates that do this).

Diesel has very extensive documentation that goes beyond the crate itself and has sections on composing applications with Diesel and best practices, extending Diesel with whatever functionality you'd like as well as how to configure the CLI. Compared to SeaORM, the docs.rs documentation has quite a lot on there! There is explicit documentation on writing queries, using the library traits, and more. In comparison, however, SeaORM has much more documentation on its own docs page which isn't based on docs.rs. Neither particularly loses in this category, although it can be slightly more difficult to find documentation about certain topics in SeaORM like ActiveModel.

Due to the way that Diesel is built, it makes very heavy use of generics. Using generics can help write crates because it can make your structs much more flexible while maintaining good performance. However, it can also result in extremely unhelpful errors when writing your application - though recent compiler improvements have made these error messages better. Other libraries (Axum, for example) have gotten around this by adding a macros flag that also allows you to add a debug_handler macro that lets you add a macro to any function that doesn't use generics to avoid the wall-of-errors issue. Like Axum, Diesel also has a macro to be able to automatically check for errors, which you can use like so:

#[derive(Selectable)]
pub struct SomeStruct {
    #[diesel(check_for_backend(diesel::pg::Pg))]
    some_field: String
}

This automatically allows Diesel to type-check your struct without you needing to do anything. Diesel also has documentation for understanding compile-time error messages dedicated to helping you tackle the various trait-related errors.

Using Diesel with Shuttle

At the moment Diesel isn't supported out of the box, but a community plugin for using Diesel with Shuttle has been created to allow you to use Diesel (via diesel-async) with Shuttle natively.

To use it, you need to run the following command:

cargo add shuttle-diesel-async --git <https://github.com/aumetra/shuttle-diesel-async>
cargo add diesel-async

Then you can add it to your code like so:

use diesel_async::{
    pooled_connection::deadpool::Pool,
    AsyncPgConnection,
};

#[shuttle_runtime::main]
async fn axum(
    #[shuttle_diesel_async::Postgres] pg: Pool<AsyncPgConnection>
) -> shuttle_axum::ShuttleAxum {
    // .. your code
}

What Should You Use?

This table illustrates the main differences between SeaORM, Diesel, and SQLx for those who just want a comparison:

LibrarySeaORMDieselSQLx
TypeFull ORMQuery builder / Data mapperRaw SQL with macros
MigrationsYesYesYes (via sqlx-cli)
Query buildingYesYesNo (raw SQL)
ModelsYes (auto-generated)Yes (manual via derive)Manual struct mapping
Lazy loadingYesNoNo
Compile time checksNo (runtime only)YesYes (via query! macros)
Raw SQL supportYes (escape hatches)YesYes (primary interface)
Extendable?Not particularly although you can extend the ActiveModelsYes - you can extend Diesel as well as the CLIN/A
Async friendly?Yes (native)Plugins requiredYes (native)
PerformanceSlower than DieselFast, optimizedFast, no abstraction overhead
Learning curveModerate (ORM patterns)Steep (generics, type system)Easy (if you know SQL)
TransparencyLow (abstracted queries)Medium (query builder)High (write actual SQL)
Best forComplex models, team needs ORM patternsType safety, compile-time guaranteesDirect control, simple queries

Below, we'll also go through some of the other major changes that differentiate SeaORM and Diesel from each other.

SeaORM is a more complete ORM experience compared to Diesel. However, it also requires more setup and boilerplate writing. Depending on how you feel about writing boilerplate, this can be a turnoff. In exchange for this, however, it allows you to slim down the application code by using the crate instead of having to implement things yourself.

Compared to SeaORM, Diesel has a larger community, with more GitHub stars. Diesel's main communities are on Gitter and GitHub Discussions. However, this may be somewhat less accessible for some users depending on if you use Gitter. SeaORM uses Discord in comparison which is more popular generally (and therefore easier to access), but there aren't as many people.

It's worth noting that Diesel has a steeper learning curve, particularly if you're not familiar with complex generic types and language theory concepts. Compile times can also increase significantly when using Diesel, though it offers better runtime performance in exchange.

Because Diesel is a smaller library and is primarily intended to be used as a query builder and data mapper, the library is a bit more barebones and leaves more to the user. However, you can also extend Diesel itself to include whatever behaviour you'd like. Some extensions have been added as community crates - which while great, is not particularly helpful if you are working within an environment that requires vetting of crates before usage. On the other hand, SeaORM doesn't allow any extension at all.

Ultimately, what you should use depends on your use case. If you want an ORM that can take care of a lot of different responsibilities in your application, you should use SeaORM. If you want to use a smaller and more extensible crate with better performance, Diesel is likely to be better. However, before committing to either, you might want to consider whether you need an ORM at all.

SQLx

SQLx GitHub repositorySQLx GitHub repository

SQLx is an async, pure-Rust SQL library that takes a different approach from traditional ORMs - it lets you write raw SQL while providing compile-time checking and type safety. Rather than abstracting SQL away, SQLx gives you direct control over your queries while catching errors at compile time through its query! and query_as! macros.

SQLx supports multiple databases (Postgres, MySQL, MariaDB, SQLite) and provides connection pooling out of the box. The library is designed to work with async Rust and integrates smoothly with web frameworks like Axum and Actix.

The main advantage of SQLx is transparency. You write actual SQL queries, so you know exactly what's being executed against your database. The query!() macro verifies your SQL at compile time by connecting to a development database and checking that columns exist, types match, and the query is valid. This catches bugs like typos, wrong column types, or missing tables before your code ever runs.

For straightforward queries, SQLx is minimal and direct. For complex queries with joins and subqueries, you're working in SQL rather than learning an ORM's query builder syntax. The tradeoff is that you need to know SQL, but you avoid the abstraction layer that can make debugging difficult with traditional ORMs.

SQLx also provides runtime query execution through query() for dynamic queries where compile-time checking isn't possible. The library includes migration support via the sqlx-cli tool, making it easy to version and manage your database schema.

Using SQLx with Shuttle

Shuttle provides first-class support for SQLx through the shuttle-shared-db crate. You can get a connection pool provisioned automatically:

#[shuttle_runtime::main]
async fn axum(
    #[shuttle_shared_db::Postgres] pool: PgPool,
) -> shuttle_axum::ShuttleAxum {
    sqlx::migrate!()
        .run(&pool)
        .await
        .expect("Failed to run migrations");

    let router = Router::new()
        .route("/", get(hello))
        .with_state(pool);

    Ok(router.into())
}

In development, Shuttle can spin up a local Postgres instance in Docker automatically. In production, it provisions a managed database on Shuttle automatically. The sqlx::migrate!() macro runs your migrations from the migrations/ directory at startup. Learn more about Shuttle databases.

Why Not Use an ORM?

There's a significant sentiment in the Rust community that questions whether ORMs are the right solution for database interactions. The core argument is that ORMs add an additional layer of abstraction that can introduce complexity rather than reduce it.

When you use raw SQL (particularly with libraries like SQLx that provide compile-time checking), you get transparency and direct control over your database queries. This makes debugging performance issues much easier because you can see exactly what queries are being executed. With an ORM, problematic queries can be hidden behind abstraction layers, making it harder to identify bottlenecks or inefficiencies.

SQLx's query! and query_as! macros provide compile-time checking that catches subtle bugs like wrong column types, missing constraints, and dead code - issues that ORMs without compile-time checking might miss. The feedback loop is fast (1-2 seconds) compared to running a full test suite, which encourages refactoring and makes developers more willing to modify database schemas.

For simple CRUD operations, ORMs can be convenient. They handle basic inserts, updates, and deletes with minimal boilerplate. But once you need complex queries involving multiple joins, subqueries, or database-specific features, you often end up fighting the ORM to express what would be straightforward in SQL. At that point, you're learning both SQL and the ORM's query builder syntax, effectively dealing with two problems instead of one.

Raw SQL also offers better performance characteristics. There's no translation layer between your code and the database, and you have complete control over query optimization. For applications where performance matters, this can make a noticeable difference.

The myth of database portability is worth addressing as well. While ORMs theoretically let you switch databases easily, in practice this rarely works smoothly. Even basic operations like INSERT RETURNING work differently across databases. If you ever need to switch from MySQL to Postgres or MongoDB to Postgres, it's going to be painful regardless of whether you use an ORM. Diesel explicitly acknowledges this by not prioritizing database portability - instead, it exposes database-specific features and lets you use them.

That said, raw SQL isn't always the answer. If you're working with a team where not everyone is comfortable with SQL, an ORM can help level the playing field. If you're building a prototype and need to move quickly, an ORM's scaffolding can speed up development. If your queries are straightforward and your schema is stable, the convenience of an ORM might outweigh its drawbacks.

The decision comes down to your specific context. If you value transparency, performance, and direct control, raw SQL with a library like SQLx is worth considering. If you value convenience and are willing to trade some performance and debuggability for it, an ORM might be the right choice. Just be aware of the tradeoffs you're making.

Finishing Up

Thanks for reading! I hope you have gained a better understanding of what Rust ORM you'd like to use for your application.

Want to get started building a database-backed Rust API? Try our quickstart template:

shuttle init --from shuttle-hq/shuttle-examples --subfolder axum/postgres

Further reading:

Share article

Get Shuttle blog posts in your inbox

We'll send you complete blog posts via email - tutorials, guides, collaborations, and product updates delivered straight to your inbox.
rocket

Build the Future of Backend Development with us

Join the movement and help revolutionize the world of backend development. Together, we can create the future!