Rust CRUD Rest API, using Axum, sqlx, Postgres, Docker and Docker Compose

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • MyrinNew
    Senior Member
    • Feb 2024
    • 5175

    #1

    Rust CRUD Rest API, using Axum, sqlx, Postgres, Docker and Docker Compose

    Let's create a CRUD Rest API in Rust, using the following:
    • Axum (Rust web framework)
    • sqlx (ORM)
    • Postgres (database)
    • Docker (containerization)
    • Docker Compose (to run the application and the database in containers)


    If you prefer a video version:







    All the code is available in the GitHub repository (link in the video description):





    🏁 Intro

    Here is a schema of the architecture of the application we are going to create:





    We will create 5 endpoints for basic CRUD operations:
    • Create
    • Read all
    • Read one
    • Update
    • Delete


    We will also create a simple endpoint to test if the server is running.


    Here are the steps we are going through:
    • Create a compose.yml file and ru the Postgres instance
    • Create an Axum application using sqlx as an ORM
    • Dockerize the Axum application
    • Run the Axum application using docker compose
    • Test the application


    We will go with a step-by-step guide, so you can follow along.





    πŸ›‘ Prerequisites

    Before starting, make sure you have the following installed on your machine:
    • Docker
    • Rust and Cargo





    🏁 Project initialization

    Let's iitialize the project:






    cargo new axumlive







    This will create a new folder called axumlive with the following structure:






    axumlive
    β”‚ Cargo.toml
    └───src
    β”‚ main.rs







    Run the Postgres container

    Let's run the Postgres container first, so we have the database ready when we will run the Axum application.


    To do that, create a file called compose.yml in the root of the project (axumlive folder) with the following content:






    db:
    image: postgres:15
    container_name: db
    ports:
    - "5432:5432"
    environment:
    POSTGRES_USER: user
    POSTGRES_PASSWORD: password
    POSTGRES_DB: simple_api
    volumes:
    - pg_data:/var/lib/postgresql/data

    volumes:
    pg_data:







    Now, to run the Postgres container, type:






    docker compose up -d db







    This will run the Postgres container in detached mode (in the background).


    If you see something like this, it means that the container is running:





    Now, to check if the container is running, type:






    docker ps -a







    If everything is ok, you should see something like this:





    Now before we write our Axum application, let's step inside the Postgres container:






    docker exec -it db psql -U user -d simple_api







    This will open the Postgres shell.


    You should see something like this:





    Now type:






    \dt







    And you should see "didn't find any relations" because the database is empty.





    This is normal but let's keep this terminal open somewhere. We will use it later to check if the tables are created correctly.





    πŸ“¦ Add dependencies and the Sql migrations

    Let's add the dependencies we need to the Cargo.toml file.






    ...
    [dependencies]
    axum = "0.8"
    tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
    sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "macros"] }
    serde = { version = "1", features = ["derive"] }
    serde_json = "1"







    Since we want a convenient way to create the tables in the database, we will use sqlx migrations. This allows us to create the table the first time we run the application. This is also convenient in case someone else wants to run the application, they just have to run the migrations and the tables will be created automatically.


    Now, let's create a folder called migrations in the root of the project (axumlive folder) and inside it create a file called 0001_users_table.sql with the following content:






    CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE
    );







    We are now ready to write the Axum application.


    πŸ“¦ Add The Dependncies

    Open the Cargo.toml file and add the following dependencies:






    [dependencies]
    axum = "0.8"
    tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
    sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "macros"] }
    serde = { version = "1", features = ["derive"] }
    serde_json = "1"










    πŸ¦€ Create the Axum backend

    Let's open the main.rs file and populate it with the following code:






    use axum::{ extract::{ Path, State }, http::StatusCode, routing::{ get, post }, Json, Router };
    use serde::{ Deserialize, Serialize };
    use sqlx::{ postgres::PgPoolOptions, FromRow, PgPool };
    use std::env;

    #[derive(Deserialize)]
    struct UserPayload {
    name: String,
    email: String,
    }

    #[derive(Serialize, FromRow)]
    struct User {
    id: i32,
    name: String,
    email: String,
    }

    #[tokio::main]
    async fn main() {
    let db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
    let pool = PgPoolOptions::new().connect(&db_url).await.expect ("Failed to connect to DB");
    sqlx::migrate!().run(&pool).await.expect("Migratio ns failed");

    let app = Router::new()
    .route("/", get(root))
    .route("/users", post(create_user).get(list_users))
    .route("/users/{id}", get(get_user).put(update_user).delete(delete_user) )
    .with_state(pool);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").awai t.unwrap();
    println!("πŸš€ Server running on port 8000");
    axum::serve(listener, app).await.unwrap();
    }

    //Endpoint Handlers
    //test endpoint
    async fn root() -> &'static str {
    "Welcome to the User Management API!"
    }

    //GET ALL
    async fn list_users(State(pool): State<PgPool>) -> Result<Json<Vec<User>>, StatusCode> {
    sqlx::query_as::<_, User>("SELECT * FROM users")
    .fetch_all(&pool).await
    .map(Json)
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
    }

    //CREATE USER
    async fn create_user(
    State(pool): State<PgPool>,
    Json(payload): Json<UserPayload>
    ) -> Result<(StatusCode, Json<User>), StatusCode> {
    sqlx::query_as::<_, User>("INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *")
    .bind(payload.name)
    .bind(payload.email)
    .fetch_one(&pool).await
    .map(|u| (StatusCode::CREATED, Json(u)))
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
    }

    //GET USER BY ID
    async fn get_user(
    State(pool): State<PgPool>,
    Path(id): Path<i32>
    ) -> Result<Json<User>, StatusCode> {
    sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
    .bind(id)
    .fetch_one(&pool).await
    .map(Json)
    .map_err(|_| StatusCode::NOT_FOUND)
    }

    //UPDATE USER
    async fn update_user(
    State(pool): State<PgPool>,
    Path(id): Path<i32>,
    Json(payload): Json<UserPayload>
    ) -> Result<Json<User>, StatusCode> {
    sqlx::query_as::<_, User>("UPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING *")
    .bind(payload.name)
    .bind(payload.email)
    .bind(id)
    .fetch_one(&pool).await
    .map(Json)
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
    }

    //DELETE USER
    async fn delete_user(
    State(pool): State<PgPool>,
    Path(id): Path<i32>
    ) -> Result<StatusCode, StatusCode> {
    let result = sqlx
    ::query("DELETE FROM users WHERE id = $1")
    .bind(id)
    .execute(&pool).await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;

    if result.rows_affected() == 0 {
    Err(StatusCode::NOT_FOUND)
    } else {
    Ok(StatusCode::NO_CONTENT)
    }
    }







    Explanation:
    • We define the data structures for the user payload and the user model using Serde for serialization and deserialization.
    • We set up the database connection using sqlx and run the migrations to create the users table
    • We define the Axum routes for the CRUD operations and associate them with their respective handler functions
    • Each handler function interacts with the database using sqlx to perform the required operations and returns appropriate HTTP responses.
    • Finally, we start the Axum server on port 8000.
      ___
      ## 🐳 Dockerize the Axum application


    It's time to Dockerize the Rust application. To do this, first let's create a .dockerignore file in the root of the project (axumlive folder) with the following content:






    target
    .git







    This will prevent Docker from copying the target folder and the .git folder into the Docker image, which are not needed.


    In the root folder of the project (axumlive), create a file called Dockerfile

    This one will be a multi-stage Dockerfile, to keep the final image as small as possible.


    Let's populate the Dockerfile


    Dockerfile:






    #stage 1
    FROM rust:1.91 as builder
    WORKDIR /app
    COPY . .
    RUN cargo build --release


    #stage 2
    FROM debian:bookworm-slim
    WORKDIR /app
    COPY --from=builder /app/target/release/axumlive .
    EXPOSE 8000
    CMD ["./axumlive"]










    🐳🐳Docker compose

    Since now we defined the Axum application and we dockerized it, it's time to add this service to the compose.yml file, so we can run both the Axum application and the Postgres database with a single command.


    Populate the docker-compose.yml file:






    services:
    app:
    container_name: simple_axum
    build: .
    ports:
    - "8000:8000"
    environment:
    DATABASE_URL: postgres://userassword@db:5432/simple_api
    db:
    image: postgres:15
    container_name: db
    ports:
    - "5432:5432"
    environment:
    POSTGRES_USER: user
    POSTGRES_PASSWORD: password
    POSTGRES_DB: simple_api
    volumes:
    - pg_data:/var/lib/postgresql/data

    volumes:
    pg_data:







    We defined a new service called "app" which is the Axum application. We set the build context to the current directory (.) and we map the port 8000 of the container to the port 8000 of the host machine.





    πŸ‘Ÿ Build and Run the Axum appliation

    Now let's build so we can run the Axum application.


    Let's type






    docker compose build







    And you should see something like this:





    This should BUILD the app image, with the name defined in the "image" value, in this case, ""simple_axum".


    You can also see all the steps docker did to build the image, layer by layer. You might recognize some of them, because we defined them in the Dockerfile.


    Now let's run the app service:






    docker compose up -d app







    This will run the app service in detached mode (in the background).





    Let's check if both the containrs are running:






    docker ps -a







    Important step: is you go back to the psql terminal where we stepped into the Postgres container, and type:






    \dt







    you should now see the users table created by the migration:





    Of course, this talbe is currenlty empty, but at least we know that the migration ran successfully when the Axum application started.





    πŸ” Test the application

    Now let's test our application.


    The first step is to visit http://localhost:8000/ in your browser:





    You can tet it in different ways. In this specific case, I will use a VS Code extension called REST Client, but you can use Postman, curl or any other tool you prefer.


    You can test the application in the way you prefer, but for convenience I will add a file you can copy paste in your application at the root level, called request.hhtp, with all the requests we need to test the applicationL






    // requests.http
    @baseUrl = http://localhost:8000

    ### 1. Test Root Endpoint (Health Check)
    GET {{baseUrl}}/

    ### 2. Get All Users (Should be empty initially)
    GET {{baseUrl}}/users

    ### 3. Create User 1
    POST {{baseUrl}}/users
    Content-Type: application/json

    {
    "name": "Alice Smith",
    "email": "alice@example.com"
    }

    ### 4. Create User 2
    POST {{baseUrl}}/users
    Content-Type: application/json

    {
    "name": "Bob Jones",
    "email": "bob@example.com"
    }

    ### 5. Create User 3
    POST {{baseUrl}}/users
    Content-Type: application/json

    {
    "name": "Charlie Day",
    "email": "charlie@example.com"
    }

    ### 6. Get All Users (Should see 3 users now)
    GET {{baseUrl}}/users

    ### 7. Get Single User (Assuming ID 1 exists)
    GET {{baseUrl}}/users/1

    ### 8. Get User that does not exist (Test 404)
    GET {{baseUrl}}/users/9999

    ### 9. Update User 2
    # Note: Ensure your backend handles PUT or PATCH for updates
    PUT {{baseUrl}}/users/2
    Content-Type: application/json

    {
    "name": "Bob James",
    "email": "bobjames@example.com"
    }

    ### 10. Get All Users (Verify Update)
    GET {{baseUrl}}/users

    ### 11. Delete User 2
    DELETE {{baseUrl}}/users/3

    ### 12. Get All Users (Final check - Bob should be gone)
    GET {{baseUrl}}/users







    You can test the example Endoiunt by runnig the first request in the file

    , which is a GET request to localhost:8000/






    πŸ“ Create a user




    Now let's create a user, making a POST request to localhost:8000/users with the body below as a request body:





    Let's create another one:





    One more:






    πŸ“ Get all users




    Now, let's make a GET request to localhost:8000/users to get all the users:





    We just created 3 users.


    From the browser, we can see:





    If we go back to the psql terminal where we stepped into the Postgres container, and type:






    SELECT * FROM users;







    we should see the 3 users we just created:








    πŸ“ Get a specific user




    If you want to get a specific user, you can make a GET request to localhost:4000/users/.


    For example, to get the user with id 1, you can make a GET request to localhost:8000/users/1





    If we try to get a user that does not exist, for example the user with id 9999, we should get a 404 error:






    πŸ“ Update a user




    If you want to update a user, you can make a PUT request to localhost:8000/users/.


    For example, to update the user with id 2, you can make a PUT request to localhost:8000/users/2 with the body below as a request body:






    πŸ“ Delete a user




    To delete a user, you can make a DELETE request to localhost:4000/users/.


    For Example, to delete the user with id 3, you can make a DELETE request to localhost:8000/users/3





    To check if the user has been deleted, you can make a GET request to localhost:8000/users or check directly in the browser:





    As you can see the user with id 3 is not there anymore.



    🏁 Conclusion




    We made it! We have built a CRUD rest API in Rust using Axum, sqlx, and Postgres, and we dockerized the application.


    This is just an example, but you can use this as a starting point to build your own application.


    If you prefer a video version:







    All the code is available in the GitHub repository (link in the video description): https://youtu.be/cJyl9e2oqHY


    That's all.


    If you have any question, drop a comment below.


    Francesco




    More...
Working...