Rust API - Creating the Get All Route with Rust - Part IV

Rust API - Creating the Get All Route with Rust - Part IV


Well, let’s continue our work. I think from here on it will be smoother. At this point we want to add our get_all_tasks route, which will be responsible for fetching all the tasks we created with our create_task route.

To start, let’s add our filter options to our schema.rs, which will be responsible for allowing us to paginate our application in the end:

  #[derive(Deserialize, Debug)]
  pub struct FilterOptions {
      pub page: Option<usize>,
      pub limit: Option<usize>,
  }

Then we’ll go to our service file, services, where we’ll create another route:

  pub async fn get_all_tasks(
    opts: Query<FilterOptions>,
    data: Data<AppState>
  ) -> impl Responder {}

After that let’s start working on our service, importing our FilterOptions and calling our Query in our actix_web which will be used for the part of our select with options:

use crate::{
    model::TaskModel,
    schema:: {
        CreateTaskSchema,
        FilterOptions
    }, AppState
};
use actix_web::{
    get,
    post,
    web::{
        Data,
        Json,
        scope,
        Query,
        ServiceConfig
    },
    HttpResponse,
    Responder
};

And define the base of our limit and offset for our application:

  pub async fn get_all_tasks(
    opts: Query<FilterOptions>,
    data: Data<AppState>
  ) -> impl Responder {
    let limit = opts.limit.unwrap_or(10);
    let offset = (opts.page.unwrap_or(1) - 1) * limit;

  }

The next step is to create our search script passing our limit, offset and make our SELECT with message handling and error handling:

 match
    sqlx
      ::query_as!(
          TaskModel,
          "SELECT * FROM tasks ORDER by id LIMIT $1 OFFSET $2",
          limit as i32,
          offset as i32
      )
      .fetch_all(&data.db)
      .await {
          Ok(tasks) => {
            let json_response = json!({
                "status": "success",
                "result":  tasks.len(),
                "tasks": tasks
            });

            return HttpResponse::Ok().json(json_response);
          }
          Err(error) => {
              return HttpResponse::InternalServerError().json(
                  json!({
                      "status": "error",
                      "message": format!("{:?}", error)
                  })
              )
          }
    }

The entire function looks like this:

#[get("/tasks")]
pub async fn get_all_tasks(opts: Query<FilterOptions>, data: Data<AppState>) -> impl Responder {
    let limit = opts.limit.unwrap_or(10);
    let offset = (opts.page.unwrap_or(1) - 1) * limit;

    match
        sqlx
            ::query_as!(
                TaskModel,
                "SELECT * FROM tasks ORDER by id LIMIT $1 OFFSET $2",
                limit as i32,
                offset as i32
            )
            .fetch_all(&data.db)
            .await {
                Ok(tasks) => {
                  let json_response = json!({
                      "status": "success",
                      "result":  tasks.len(),
                      "tasks": tasks
                  });

                  return HttpResponse::Ok().json(json_response);
                }
                Err(error) => {
                    return HttpResponse::InternalServerError().json(
                        json!({
                            "status": "error",
                            "message": format!("{:?}", error)
                        })
                    )
                }
            }
}

At the end of our file, we just need to add our route to the public function we have with our configurations:

pub fn config(conf: &mut ServiceConfig) {
    let scope = scope("/api")
        .service(health_checker)
        .service(create_task)
        .service(get_all_tasks);

    conf.service(scope);
}

In the end our schema.rs looks like this:

  src/schema.rs
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct CreateTaskSchema {
    pub title: String,
    pub content: String,
}

#[derive(Deserialize, Debug)]
pub struct FilterOptions {
    pub page: Option<usize>,
    pub limit: Option<usize>,
}

And our services.rs as a whole will look like this:

  src/services.rs
use crate::{
    model::TaskModel,
    schema:: {
        CreateTaskSchema,
        FilterOptions
    }, AppState
};
use actix_web::{
    get,
    post,
    web::{
        Data,
        Json,
        scope,
        Query,
        ServiceConfig
    },
    HttpResponse,
    Responder
};
use serde_json::json;

#[get("/healthchecker")]
async fn health_checker() -> impl Responder {
    const MESSAGE: &str = "Health check API is up and running smoothly.";
    HttpResponse::Ok().json(json!({"status": "success", "message": MESSAGE }))
}

#[post("/task")]
async fn create_task(body: Json<CreateTaskSchema>, data: Data<AppState>) -> impl Responder {
    match
        sqlx
            ::query_as!(
                TaskModel,
                "INSERT INTO tasks (title, content) VALUES ($1, $2)
                RETURNING * ",
                body.title.to_string(),
                body.content.to_string()
            )
            .fetch_one(&data.db)
            .await {
            Ok(tasks) => {
              let json_response = json!({
                  "status": "success",
                  "result":  tasks.len(),
                  "tasks": tasks
              });

              return HttpResponse::Ok().json(json_response);
            }
            Err(error) => {
                return HttpResponse::InternalServerError().json(
                    json!({
                        "status": "error",
                        "message": format!("{:?}", error)
                    })
                )
            }
        }
}

#[get("/tasks")]
pub async fn get_all_tasks(opts: Query<FilterOptions>, data: Data<AppState>) -> impl Responder {
    let limit = opts.limit.unwrap_or(10);
    let offset = (opts.page.unwrap_or(1) - 1) * limit;

    match
        sqlx
            ::query_as!(
                TaskModel,
                "SELECT * FROM tasks ORDER by id LIMIT $1 OFFSET $2",
                limit as i32,
                offset as i32
            )
            .fetch_all(&data.db)
            .await {
                Ok(tasks) => {
                  let json_response = json!({
                      "status": "success",
                      "result":  tasks.len(),
                      "tasks": tasks
                  });

                  return HttpResponse::Ok().json(json_response);
                }
                Err(error) => {
                    return HttpResponse::InternalServerError().json(
                        json!({
                            "status": "error",
                            "message": format!("{:?}", error)
                        })
                    )
                }
            }
}

pub fn config(conf: &mut ServiceConfig) {
    let scope = scope("/api")
        .service(health_checker)
        .service(create_task)
        .service(get_all_tasks);

    conf.service(scope);
}

Testing, you can use insomnia pointing to the following route: http://localhost:8080/api/tasks

Or with curl, testing directly from your terminal:

curl --request GET \
  --url http://localhost:8080/api/tasks

And if everything went right you’ll get the following message:

{
  "result": 2,
  "status": "success",
  "tasks": [
    {
      "content": "content test",
      "created_at": "2023-06-08T22:50:44.186036Z",
      "id": "dc512907-fbdb-4f97-b130-c0859dc2f2ef",
      "title": "title test2"
    },
    {
      "content": "content test",
      "created_at": "2023-06-08T21:41:25.586259Z",
      "id": "fc7e46df-8dd4-40a5-87f0-65d097d31e5a",
      "title": "title test"
    }
  ]
}

Next step we should create a new route to fetch a specific task and delete it. See you later!