🎃

Rust REST APIとDBをSQLxで接続する

2024/09/24に公開

概要

今回は英語で記事を書いていきます。
Today we're going to connect an existing Actix Web REST API and a Postgres DB with the SQLx Rust crate. This tutorial assumes you have Rust installed already.
To learn how to setup an Actix Web REST API, check out my previous article: Fast REST API with Rust and Actix Web.
Before we begin, make sure you have your DATABASE_URL set in your .env file. An example can be seen in the .env.example file.

First we need to add our dependencies:

> cargo add dotenv sqlx --features="sqlx/postgres, sqlx/runtime-async-std, sqlx/tls-rustls" 

Now, in our /src/main.rs we need to create our Postgres connection pool and share it with our routes:

2. use dotenv::dotenv;
3. use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
8. // information shared with each route
9. struct AppState {
10.     db: Pool<Postgres>,
11. }
21. // configure our API to utilize the .env file
22. dotenv().ok();
23. 
24. // retrieve the database url from the .env file
25. let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set!");
26. 
27. // create a connection pool to the database
28. let pool = PgPoolOptions::new()
29.     .max_connections(5)
30.     .connect(&database_url)
31.     .await
32.     .expect("Error building a connection pool");
36. // share the pool with each route
37. .app_data(Data::new(AppState { db: pool.clone() }))

/src/todolist/services.rs ファイルには、いくつかのルートが定義されています。
In our /src/todolist/services.rs file we have a few routes defined:

  • GET /todolist/entries
  • POST /todolist/entries
  • PUT /todolist/entries/{id}
  • DELETE /todolist/entries/{id}

Before we can hook each route up to our Postgres database, we need to perform some setup.
First we need to make sure our database has the appropriate tables:

CREATE TABLE todolist_entries (
    id SERIAL PRIMARY KEY,
    title VARCHAR(255),
    complete BOOLEAN DEFAULT FALSE
);

Next, we need to allow deserialization of query results into our TodolistEntry struct location in /src/todolist/models.rs

2. // FromRow allows deserialization of query results into Rust structs
3.        use sqlx::FromRow;

And add FromRow to our TodolistEntry derive macro list:

#[derive(Serialize, Deserialize, Clone, FromRow)]

Now it's time to hook our routes up. Open up /src/todolist/services.rs.
SQLx dependency import:

4. use sqlx;

GET /todolist/entries:

6. #[get("/todolist/entries")]
7. async fn get_entries(data: web::Data<AppState>) -> impl Responder {
8.     match sqlx::query_as::<_, TodolistEntry>("SELECT id, title, complete FROM todolist_entries")
9.         // return all matching rows
10.        .fetch_all(&data.db)
11.        .await
12.    {
13.        Ok(entries) => HttpResponse::Ok().json(entries),
14.        Err(_) => HttpResponse::InternalServerError().json("Error trying to get entries"),
15.    }
16. }

POST /todolist/entries:

18. #[post("/todolist/entries")]
19. async fn create_entry(
20.     data: web::Data<AppState>,
21.     body: web::Json<CreateEntryBody>,
22. ) -> impl Responder {
23.     let param_obj = body.into_inner();
24.
25.     match sqlx::query_as::<_, TodolistEntry>("INSERT INTO todolist_entries (title) VALUES ($1) RETURNING id, title, complete")
26.         // bind the param_obj.title value to the $1 argument
27.         .bind(&param_obj.title)
28.         // return one row
29.         .fetch_one(&data.db)
30.         .await
31.     {
32.         Ok(entry) => HttpResponse::Ok().json(entry),
33.         Err(_) => HttpResponse::InternalServerError().json("Error trying to create entry"),
34.     }
35. }

PUT /todolist/entries/{id}:

39. #[put("/todolist/entries/{id}")]
40. async fn update_entry(
41.     data: web::Data<AppState>,
42.     path: web::Path<i32>,
43.     body: web::Json<CompleteEntryBody>,
44. ) -> impl Responder {
45.     let id = path.into_inner();
46.     let param_obj = body.into_inner();
47. 
48.     match sqlx::query_as::<_, TodolistEntry>("UPDATE todolist_entries SET complete=$1 WHERE id=$2 RETURNING id, title, complete")
49.         // bind the param_obj.complete value to the $1 argument
50.         .bind(&param_obj.complete)
51.         // bind the id value to the $2 argument
52.         .bind(&id)
53.         // return the updated row
54.         .fetch_one(&data.db)
55.         .await
56.     {
57.         Ok(entry) => HttpResponse::Ok().json(entry),
58.         Err(_) => HttpResponse::InternalServerError().json("Error trying to update entry"),
59.     }
60. }

DELETE /todolist/entries/{id}:

62. #[delete("/todolist/entries/{id}")]
63. async fn delete_entry(data: web::Data<AppState>, path: web::Path<i32>) -> impl Responder {
64.     let id = path.into_inner();
65.
66.     match sqlx::query!(
67.         "DELETE FROM todolist_entries WHERE id=$1",
68.         // bind the id value to the $1 argument
69.         id
70.     )
71.         // execute the query without returning any information
72.         .execute(&data.db)
73.         .await
74.     {
75.         Ok(_) => HttpResponse::Ok().json("Successfully deleted entry"),
76.         Err(_) => HttpResponse::InternalServerError().json("Error trying to delete entry"),
77.     }
78. }

And with that, you're ready to give your new server a test drive with your favorite HTTP request method. I personally prefer using the VS Code extension Thunder Client .

Full code can be found at https://github.com/colessar/Rust-REST-API-with-SQLx-Actix-Web

Questions about the article: click here


ありがとうございます

Discussion