Starter Axum, GraphQL and SeaORM Template
Tags: rust, graphql, seaorm, axum
Published: 2022-03-16 16:04:04

👋 Hello! I recently contributed to [SeaORM](https://github.com/SeaQL/sea-orm) and added a starter template I created for bootstrapping APIs using [Axum](https://github.com/tokio-rs/axum), GraphQL and [SeaORM](https://github.com/SeaQL/sea-orm). I wanted to share this here to:

- give SeaORM some more exposure (it's a great ORM!)
- help out some people wanting to try this stack
- get some feedback to help me grow

In this post I'll give an overview of how to create the template from scratch, however the final template is available [here](https://github.com/SeaQL/sea-orm/tree/master/examples/axum-graphql_example) if you'd like to just skip ahead past the "how I made this" part and take a look.

## Project Structure 🧬

```
.
├── Cargo.lock
├── Cargo.toml
├── db
├── entity
│   ├── Cargo.toml
│   └── src
│       ├── lib.rs
│       └── note.rs
├── migration
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── README.md
│   └── src
│       ├── lib.rs
│       ├── m20220101_000001_create_table.rs
│       └── main.rs
└── src
    ├── db.rs
    ├── graphql
    │   ├── mod.rs
    │   ├── mutation
    │   │   ├── mod.rs
    │   │   └── note.rs
    │   ├── query
    │   │   ├── mod.rs
    │   │   └── note.rs
    │   └── schema.rs
    └── main.rs

8 directories, 20 file
```

## Prerequisites 🥸

Before we get started, be sure to have rust/cargo properly installed on your machine. Then, we'll install some additional dependencies:

```bash
cargo install cargo-watch sea-orm-cli
```

_Note: `sea-orm-cli` [might change soon](https://github.com/SeaQL/sea-orm/pull/558)._

I use `cargo-watch` on all my rust web projects so that the server restarts/recompiles on file changes. `sea-orm-cli` is a CLI utility that will help us create our migrations, although there are more functionalities available.

## Setup 🔨

As shown in the Project Structure section, this template will have 2 additional crates:

1. entity: where our database models are defined
2. migration: where are database migrations are defined

To get started, we will create a barebones Rust project:

```bash
cargo new axum-graphql-seaorm
```

This is just the standard hello world starter template. Now, we want to create the migration and entity crates. We'll start with the migrations.

`cd` into the project folder and run the following command:

```bash
sea-orm-cli migrate init
```

This will create the `migrations` directory, with the boilerplate migration structure. To see more information about this, take a look at the corresponding section in [the docs](https://www.sea-ql.org/SeaORM/docs/migration/setting-up-migration).

Now let's create the entity crate:

```bash
cargo new --lib entity
```

We'll want to remove the boilerplate from this and then add the relevant dependencies. Add the following to your Cargo.toml:

```toml
[dependencies]
serde = { version = "1", features = ["derive"] }

[dependencies.async-graphql]
version = "3.0.12"

[dependencies.sea-orm]
version = "^0.6.0"
features = [
  "macros",
  "debug-print",
  "runtime-tokio-native-tls",
  "sqlx-sqlite",
]
default-features = false
```

Notice I am using the `sqlx-sqlite` feature, for this template I am using sqlite. I typically start with sqlite early on in projects for ease of development, and then transition to MySQL/Postgres later on.

As per SeaORM's suggestion of structuring projects, we will share the `sea-orm` and `async-graphql` dependencies to the other crates as to prevent accidental mixing of versions. Add the following to `lib.rs`:

```rust
pub use async_graphql;
pub use sea_orm;
```

This will allow us pull in those dependencies in our core crate or migration crate like:

```rust
use entity::{sea_orm, async_graphql};
```

## Create an entity

The relevant documentation for this section lives [here](https://www.sea-ql.org/SeaORM/docs/generate-entity/entity-structure).

Let's create our first entity. The demo API will interact with a simple collection of notes. The `note` entity will contain `title` and `text` fields.

Create a `note.rs` file with the following content:

```rust
use async_graphql::*;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize, SimpleObject)]
#[sea_orm(table_name = "notes")]
#[graphql(concrete(name = "Note", params()))]
pub struct Model {
    #[sea_orm(primary_key)]
    #[serde(skip_deserializing)]
    pub id: i32,
    pub title: String,
    pub text: String,
}

#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}

impl RelationTrait for Relation {
    fn def(&self) -> RelationDef {
        panic!("No RelationDef")
    }
}

impl ActiveModelBehavior for ActiveModel {}
```

This will define our Model and Relation for the `note` entity. Notice the derives throughout the file, but specifically the `DeriveEntityModel` and `SimpleObject`. 

`DeriveEntityModel` defines an Entity with associating Model, Column and PrimaryKey. 

`SimpleObject` will directly map the fields of the Model struct to a GraphQL object. You can even skip certain fields using `#[graphql(skip)]`, like if you had a `user` entity with a password hash, for example.

Our `note` entity now has the following graphql type definition:

```graphql
type Note {
  id: Int!
  title: String!
  text: String!
}
```

Something I like to do is define some utility functions to my entities that don't come default with the `Entity` struct. Add the following the the bottom of your `note.rs` file:

`note.rs`
```rust
impl Entity {
    pub fn find_by_id(id: i32) -> Select<Entity> {
        Self::find().filter(Column::Id.eq(id))
    }

    pub fn find_by_title(title: &str) -> Select<Entity> {
        Self::find().filter(Column::Title.eq(title))
    }

    pub fn delete_by_id(id: i32) -> DeleteMany<Entity> {
        Self::delete_many().filter(Column::Id.eq(id))
    }
}
```

And be sure to add the `DeleteMany` trait to the sea_orm use statement at the top. What this does is define some extra functions my `note::Entity` has. I do this mainly to remove code duplication and increase readability. 

Add our note entity to our `lib.rs` file and let's create our first migration.

`lib.rs`
```rust
pub mod note;
```

## Defining our initial migration

We need to add a couple of dependencies to our `migration` crate. Add the following dependencies to the `Cargo.toml` file:

```toml
dotenv = "0.15.0"
entity = { path = "../entity" }
```

While we could just hardcode the database URI, I like this little addition of `dotenv` in my templates to reduce repetition when starting new projects. Create an `.env` file at the project root, not the `migration` crate root, with the following line: 

```
DATABASE_URL=sqlite:./db?mode=rwc
```

The flags at the end are important, as seaorm needs to be to read and write our database file. Now, update your `main.rs` file in the `migration` crate to contain the following:

```rust
use migration::Migrator;
use sea_schema::migration::*;

#[cfg(debug_assertions)]
use dotenv::dotenv;

#[async_std::main]
async fn main() {
    #[cfg(debug_assertions)]
    dotenv().ok();

    let fallback = "sqlite:./db?mode=rwc";

    match std::env::var("DATABASE_URL") {
        Ok(val) => {
            println!("Using DATABASE_URL: {}", val);
        }
        Err(_) => {
            std::env::set_var("DATABASE_URL", fallback);
            println!("Set DATABASE_URL: {}", fallback);
        }
    };

    cli::run_cli(Migrator).await;
}
```

All this addition really does is set the env variable if it is not present.

Now that we have that inital set up completed, we can write the actual code for our `up` and `down` migrations in the migration file (it will have the migration stamp postfixed with something like `create_table.rs`). Add the two following utility functions:

```rust
fn get_seaorm_create_stmt<E: EntityTrait>(e: E) -> TableCreateStatement {
    let schema = Schema::new(DbBackend::Sqlite);

    schema
        .create_table_from_entity(e)
        .if_not_exists()
        .to_owned()
}

fn get_seaorm_drop_stmt<E: EntityTrait>(e: E) -> TableDropStatement {
    Table::drop().table(e).if_exists().to_owned()
}
```

I created these to reduce some repetition in my migration files. We are putting them in this migration file, however you can extract them out to a separate file if desired.

Our `up` migration will have the following content:

```rust
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        let stmts = vec![get_seaorm_create_stmt(note::Entity)];

        for stmt in stmts {
            manager.create_table(stmt.to_owned()).await?;
        }

        Ok(())
    }
```

I generate a statement vector and loop through it to create each table. I do the same for the `down` migration:

```rust
    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        let stmts = vec![get_seaorm_drop_stmt(note::Entity)];

        for stmt in stmts {
            manager.drop_table(stmt.to_owned()).await?;
        }

        Ok(())
    }
```

With our `up` and `down` migrations created, we can now run a migration to create our table in our database. At the project root, run the following:

```bash
sea-orm-cli migrate up
```

Inspecting the database you should see the `notes` table! 

At this point, we've

- created a basic structure for our server
- defined a simple `note` entity
- created an initial migration for the database

Next let's create a small Database struct to wrap SeaORM's `DatabaseConnection`, and then define our GraphQL Schema

## Database Struct

I like to wrap SeaORM's `DatabaseConnection` in my own `Database` struct. I'll paste it below:

`src/db.rs`
```rust
use entity::sea_orm;
use sea_orm::DatabaseConnection;

pub struct Database {
    pub connection: DatabaseConnection,
}

impl Database {
    pub async fn new() -> Self {
        let connection = sea_orm::Database::connect(std::env::var("DATABASE_URL").unwrap())
            .await
            .expect("Could not connect to database");

        Database { connection }
    }

    pub fn get_connection(&self) -> &DatabaseConnection {
        &self.connection
    }
}
```

I could just share the DatabaseConnection using OnceCell or directly injecting into the GraphQL context, however I prefer this method in case I want to add any additional functionality to my struct.

## Creating the Schema

How you organize your project is entirely up to you, the structure that I have been using is to define a `graphql` module, that internally has `query` and `mutation` submodules, and a `schema` module. Let's create the files we will be editing, there will be a lot so I will go a little faster here:

```bash
mkdir src/graphql
touch src/graphql/mod.rs
touch src/graphql/schema.rs

mkdir src/graphql/query
touch src/graphql/query/mod.rs
touch src/graphql/query/note.rs

mkdir src/graphql/mutation
touch src/graphql/mutation/mod.rs
touch src/graphql/mutation/note.rs
```

Now that the files are created we will:

1. create our `Query` type
2. create out `Mutation` type
3. Define our schema

### Create the Query type

Our Query type will have two queries: get *all* the notes, and get a note by an id. [Here](https://www.sea-ql.org/SeaORM/docs/basic-crud/select) you can find more information about SeaORM's select query syntax.

`src/graphql/query/note.rs`
```rust
use async_graphql::{Context, Object, Result};
use entity::{async_graphql, note, sea_orm::EntityTrait};

use crate::db::Database;

#[derive(Default)]
pub struct NoteQuery;

#[Object]
impl NoteQuery {
    async fn get_notes(&self, ctx: &Context<'_>) -> Result<Vec<note::Model>> {
        let db = ctx.data::<Database>().unwrap();

        Ok(note::Entity::find()
            .all(db.get_connection())
            .await
            .map_err(|e| e.to_string())?)
    }

    async fn get_note_by_id(&self, ctx: &Context<'_>, id: i32) -> Result<Option<note::Model>> {
        let db = ctx.data::<Database>().unwrap();

        Ok(note::Entity::find_by_id(id)
            .one(db.get_connection())
            .await
            .map_err(|e| e.to_string())?)
    }
}
```

We need to use `#[Object]` to declare our struct as a GraphQL Object. I am using a minimal error handling strategy here, as I didn't want to introduce too many dependencies in this template.

`src/graphql/query/mod.rs`
```rust
use entity::async_graphql;

pub mod note;

pub use note::NoteQuery;

// Add your other ones here to create a unified Query object
// e.x. Query(NoteQuery, OtherQuery, OtherOtherQuery)
#[derive(async_graphql::MergedObject, Default)]
pub struct Query(NoteQuery);
```

To create a unified Query GraphQL type, I am using `#[derive(async_graphql::MergedObject, Default)]`. If you had another entity `user` and created a Query object for it called `UserQuery`, you would simply just need to add it as shown in the comment examples.

This is the resulting Query type:

```graphql
type Query {
  getNotes: [Note!]!
  getNoteById(id: Int!): Note
}
```

### Create the Mutation type

Creating the Mutation type will be mostly the same:

`src/graphql/mutation/note.rs`
```rust
use async_graphql::{Context, Object, Result};
use entity::async_graphql::{self, InputObject, SimpleObject};
use entity::note;
use entity::sea_orm::{ActiveModelTrait, Set};

use crate::db::Database;

// I normally separate the input types into separate files/modules, but this is just
// a quick example.

#[derive(InputObject)]
pub struct CreateNoteInput {
    pub title: String,
    pub text: String,
}

#[derive(SimpleObject)]
pub struct DeleteResult {
    pub success: bool,
    pub rows_affected: u64,
}

#[derive(Default)]
pub struct NoteMutation;

#[Object]
impl NoteMutation {
    pub async fn create_note(
        &self,
        ctx: &Context<'_>,
        input: CreateNoteInput,
    ) -> Result<note::Model> {
        let db = ctx.data::<Database>().unwrap();

        let note = note::ActiveModel {
            title: Set(input.title),
            text: Set(input.text),
            ..Default::default()
        };

        Ok(note.insert(db.get_connection()).await?)
    }

    pub async fn delete_note(&self, ctx: &Context<'_>, id: i32) -> Result<DeleteResult> {
        let db = ctx.data::<Database>().unwrap();

        let res = note::Entity::delete_by_id(id)
            .exec(db.get_connection())
            .await?;

        if res.rows_affected <= 1 {
            Ok(DeleteResult {
                success: true,
                rows_affected: res.rows_affected,
            })
        } else {
            unimplemented!()
        }
    }
}
```

I am only adding a mutation to create and delete a note here, go ahead and try to create an update!

`src/graphql/mutation/mod.rs`
```rust
use entity::async_graphql;

pub mod note;

pub use note::NoteMutation;

// Add your other ones here to create a unified Mutation object
// e.x. Mutation(NoteMutation, OtherMutation, OtherOtherMutation)
#[derive(async_graphql::MergedObject, Default)]
pub struct Mutation(NoteMutation);
```

### Create the Schema

Now that we have defined our Mutation and Query types, let's go ahead and define our schema. To start, add the following to your `mod.rs`

`src/graphql/mod.rs`
```rust
pub mod mutation;
pub mod query;
pub mod schema;
```

We won't actually be globally *defining* our schema in the schema.rs file, instead we will create a helper function that will build and return the schema. To do so, let's create a type for our schema and the function itself:

`src/graphql/schema.rs`
```rust
use async_graphql::{EmptySubscription, Schema};
use entity::async_graphql;

use crate::{
    db::Database,
    graphql::{mutation::Mutation, query::Query},
};

pub type AppSchema = Schema<Query, Mutation, EmptySubscription>;

/// Builds the GraphQL Schema, attaching the Database to the context
pub async fn build_schema() -> AppSchema {
    let db = Database::new().await;

    Schema::build(Query::default(), Mutation::default(), EmptySubscription)
        .data(db)
        .finish()
}
```

We define the type of our schema according to our Query and Mutation types, and an empty subscription (since we don't have any subscriptions). If you had subscriptions, you would create it similar to how we created the others!

We create a database struct that the schema will manage using the `data` function call. This will inject the database struct into the GraphQL context. Looking back at either the query or mutation structs, you can see this in action: 

```rust
let db = ctx.data::<Database>().unwrap();
```

At this point:

- We have our entity defined
- We have the connection available, through Database struct
- We defined our migration (and ran it!)
- We created our Query/Mutation types, and our Schema

That pretty much wraps up everything we need to get started! All that is left is to create the server and test!

## Create the Axum server

I chose Axum mostly to learn it, I have also used Rocket in the past. Both are great, choose whichever is your preference. The syntax of Rocket is probably a little better for me personally, however.

This has gotten long, so I'll paste the code below and overview it afterwards 🙂

`src/main.rs`
```rust
mod db;
mod graphql;

use entity::async_graphql;

use async_graphql::http::{playground_source, GraphQLPlaygroundConfig};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse};
use axum::{
    extract::Extension,
    response::{Html, IntoResponse},
    routing::get,
    Router,
};
use graphql::schema::{build_schema, AppSchema};

#[cfg(debug_assertions)]
use dotenv::dotenv;

async fn graphql_handler(schema: Extension<AppSchema>, req: GraphQLRequest) -> GraphQLResponse {
    schema.execute(req.into_inner()).await.into()
}

async fn graphql_playground() -> impl IntoResponse {
    Html(playground_source(GraphQLPlaygroundConfig::new(
        "/api/graphql",
    )))
}

#[tokio::main]
async fn main() {
    #[cfg(debug_assertions)]
    dotenv().ok();

    let schema = build_schema().await;

    let app = Router::new()
        .route(
            "/api/graphql",
            get(graphql_playground).post(graphql_handler),
        )
        .layer(Extension(schema));

    println!("Playground: http://localhost:3000/api/graphql");

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}
```

`graphql_handler` actually handles the GraphQL requests, invoking the `execute` function of the schema using the request.

`graphql_playground` returns the HTML for the GraphQL Playground, this is the fancy looking version, as opposed to the `GraphiQL` version.

Notice we call the `dotenv().ok()` here, so the downstream uses, namely in `db.rs` can use the .env variables. We create the schema using the `build_schema` utility we built, and then add it as an extension layer to our axum app. I use the `/api/graphql` prefix for the `get` and `post` requests, as my brain prefers the semantics of pinging that instead of `/graphql`.

## Wrapping it all up 🌯

Our application should be all good to run now! Double check you've imported all the required traits and what not, and give it a whirl! If you plan on making changes live and wanted a hot reload, run:

```bash
cargo watch -x run
```

Otherwise just `cargo run`

Navigate to `localhost:3000/api/graphql` to get to the playground and test out the API!

`createNote`
```graphql
mutation {
  createNote(input:{title:"Noice noice note", text:"This is the note content!"}) {
    id
  }
}
```

`getNotes`
```graphql
query {
  getNotes {
    id
    title
    text
  }
}
```

`getNoteById`
```graphql
query {
  getNoteById(id:1) {
    title
    text
  }
}
```

## Tada! We're done! 🎉

Thanks for bearing with me! Let me know if this was helpful or if you have any notes (I love notes!) 

Additionally, if you're looking to dip your toes into a project that uses _some_ of this technology stack, consider contributing to my personal project [Stump](https://github.com/aaronleopold/stump) - A free and open source comic book server with OPDS support, written in Rust and React.