-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Todo API example using Actix-web and SQLx with PostgreSQL database
- Loading branch information
Showing
10 changed files
with
385 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
HOST=127.0.0.1 | ||
PORT=5000 | ||
DATABASE_URL="postgres://user:pass@192.168.33.11/actix_sqlx_todo" | ||
RUST_LOG=actix_rest_api_sqlx=info,actix=info |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
/target | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
[package] | ||
name = "actix_sqlx" | ||
version = "0.1.0" | ||
authors = ["Milan Zivkovic <zivkovic.milan@gmail.com>"] | ||
edition = "2018" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
listenfd = "0.3.3" | ||
actix-web = "3.0.0-alpha.1" | ||
actix-rt = "1.1.0" | ||
serde = "1.0.106" | ||
serde_json = "1.0.51" | ||
sqlx = { version = "0.3", features = [ "postgres" ] } | ||
futures = "0.3.4" | ||
dotenv = "0.15.0" | ||
env_logger = "0.7.1" | ||
log = "0.4.8" | ||
anyhow = "1.0.28" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# actix-sqlx-todo | ||
|
||
Example Todo API using [Actix-web](https://github.com/actix/actix-web) and SQLx with posgresql | ||
|
||
# Usage | ||
|
||
## Prerequisites | ||
|
||
* Rust | ||
* PostgreSQL | ||
|
||
## Change into the project sub-directory | ||
|
||
All instructions assume you have changed into this folder: | ||
|
||
```bash | ||
cd examples/postgres/todo-api | ||
``` | ||
|
||
## Set up the database | ||
|
||
* Create new database using `schema.sql` | ||
* Copy `.env-example` into `.env` and adjust DATABASE_URL to match your PostgreSQL address, username and password | ||
|
||
## Run the application | ||
|
||
To run the application execute: | ||
|
||
```bash | ||
cargo run | ||
``` | ||
|
||
By default application will be available on `http://localhost:5000`. If you wish to change address or port you can do it inside `.env` file. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
CREATE TABLE IF NOT EXISTS todos ( | ||
id SERIAL PRIMARY KEY, | ||
description TEXT NOT NULL, | ||
done BOOLEAN NOT NULL DEFAULT FALSE | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
#[macro_use] | ||
extern crate log; | ||
|
||
use dotenv::dotenv; | ||
use listenfd::ListenFd; | ||
use std::env; | ||
use actix_web::{web, App, HttpResponse, HttpServer, Responder}; | ||
use sqlx::PgPool; | ||
use anyhow::Result; | ||
|
||
// import todo module (routes and model) | ||
mod todo; | ||
|
||
// default / handler | ||
async fn index() -> impl Responder { | ||
HttpResponse::Ok().body(r#" | ||
Welcome to Actix-web with SQLx Todos example. | ||
Available routes: | ||
GET /todos -> list of all todos | ||
POST /todo -> create new todo, example: { "description": "learn actix and sqlx", "done": false } | ||
GET /todo/{id} -> show one todo with requested id | ||
PUT /todo/{id} -> update todo with requested id, example: { "description": "learn actix and sqlx", "done": true } | ||
DELETE /todo/{id} -> delete todo with requested id | ||
"# | ||
) | ||
} | ||
|
||
#[actix_rt::main] | ||
async fn main() -> Result<()> { | ||
dotenv().ok(); | ||
env_logger::init(); | ||
|
||
// this will enable us to keep application running during recompile: systemfd --no-pid -s http::5000 -- cargo watch -x run | ||
let mut listenfd = ListenFd::from_env(); | ||
|
||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set in .env file"); | ||
// PgPool::builder() | ||
// .max_size(5) // maximum number of connections in the pool | ||
// .build(env::var("DATABASE_URL")?).await?; | ||
let db_pool = PgPool::new(&database_url).await?; | ||
|
||
let mut server = HttpServer::new(move || { | ||
App::new() | ||
.data(db_pool.clone()) // pass database pool to application so we can access it inside handlers | ||
.route("/", web::get().to(index)) | ||
.configure(todo::init) // init todo routes | ||
}); | ||
|
||
server = match listenfd.take_tcp_listener(0)? { | ||
Some(listener) => server.listen(listener)?, | ||
None => { | ||
let host = env::var("HOST").expect("HOST is not set in .env file"); | ||
let port = env::var("PORT").expect("PORT is not set in .env file"); | ||
server.bind(format!("{}:{}", host, port))? | ||
} | ||
}; | ||
|
||
info!("Starting server"); | ||
server.run().await?; | ||
|
||
Ok(()) | ||
} | ||
|
||
// export DATABASE_URL="postgres://pguser:zx@192.168.33.11/realworld" | ||
// systemfd --no-pid -s http::5000 -- cargo watch -x run | ||
// I would add the example as "todo-api" | ||
// Under the postgres folder |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
mod model; | ||
mod routes; | ||
|
||
pub use model::*; | ||
pub use routes::init; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
use serde::{Serialize, Deserialize}; | ||
use actix_web::{HttpResponse, HttpRequest, Responder, Error}; | ||
use futures::future::{ready, Ready}; | ||
use sqlx::{PgPool, FromRow, Row}; | ||
use sqlx::postgres::PgRow; | ||
use anyhow::Result; | ||
|
||
// this struct will use to receive user input | ||
#[derive(Serialize, Deserialize)] | ||
pub struct TodoRequest { | ||
pub description: String, | ||
pub done: bool | ||
} | ||
|
||
// this struct will be used to represent database record | ||
#[derive(Serialize, FromRow)] | ||
pub struct Todo { | ||
pub id: i32, | ||
pub description: String, | ||
pub done: bool, | ||
} | ||
|
||
// implementation of Actix Responder for Todo struct so we can return Todo from action handler | ||
impl Responder for Todo { | ||
type Error = Error; | ||
type Future = Ready<Result<HttpResponse, Error>>; | ||
|
||
fn respond_to(self, _req: &HttpRequest) -> Self::Future { | ||
let body = serde_json::to_string(&self).unwrap(); | ||
// create response and set content type | ||
ready(Ok( | ||
HttpResponse::Ok() | ||
.content_type("application/json") | ||
.body(body) | ||
)) | ||
} | ||
} | ||
|
||
// Implementation for Todo struct, functions for read/write/update and delete todo from database | ||
impl Todo { | ||
pub async fn find_all(pool: &PgPool) -> Result<Vec<Todo>> { | ||
let mut todos = vec![]; | ||
let recs = sqlx::query!( | ||
r#" | ||
SELECT id, description, done | ||
FROM todos | ||
ORDER BY id | ||
"# | ||
) | ||
.fetch_all(pool) | ||
.await?; | ||
|
||
for rec in recs { | ||
todos.push(Todo { | ||
id: rec.id, | ||
description: rec.description, | ||
done: rec.done | ||
}); | ||
} | ||
|
||
Ok(todos) | ||
} | ||
|
||
pub async fn find_by_id(id: i32, pool: &PgPool) -> Result<Todo> { | ||
let rec = sqlx::query!( | ||
r#" | ||
SELECT * FROM todos WHERE id = $1 | ||
"#, | ||
id | ||
) | ||
.fetch_one(&*pool) | ||
.await?; | ||
|
||
Ok(Todo { | ||
id: rec.id, | ||
description: rec.description, | ||
done: rec.done | ||
}) | ||
} | ||
|
||
pub async fn create(todo: TodoRequest, pool: &PgPool) -> Result<Todo> { | ||
let mut tx = pool.begin().await?; | ||
let todo = sqlx::query("INSERT INTO todos (description, done) VALUES ($1, $2) RETURNING id, description, done") | ||
.bind(&todo.description) | ||
.bind(todo.done) | ||
.map(|row: PgRow| { | ||
Todo { | ||
id: row.get(0), | ||
description: row.get(1), | ||
done: row.get(2) | ||
} | ||
}) | ||
.fetch_one(&mut tx) | ||
.await?; | ||
|
||
tx.commit().await?; | ||
Ok(todo) | ||
} | ||
|
||
pub async fn update(id: i32, todo: TodoRequest, pool: &PgPool) -> Result<Todo> { | ||
let mut tx = pool.begin().await.unwrap(); | ||
let todo = sqlx::query("UPDATE todos SET description = $1, done = $2 WHERE id = $3 RETURNING id, description, done") | ||
.bind(&todo.description) | ||
.bind(todo.done) | ||
.bind(id) | ||
.map(|row: PgRow| { | ||
Todo { | ||
id: row.get(0), | ||
description: row.get(1), | ||
done: row.get(2) | ||
} | ||
}) | ||
.fetch_one(&mut tx) | ||
.await?; | ||
|
||
tx.commit().await.unwrap(); | ||
Ok(todo) | ||
} | ||
|
||
pub async fn delete(id: i32, pool: &PgPool) -> Result<u64> { | ||
let mut tx = pool.begin().await?; | ||
let deleted = sqlx::query("DELETE FROM todos WHERE id = $1") | ||
.bind(id) | ||
.execute(&mut tx) | ||
.await?; | ||
|
||
tx.commit().await?; | ||
Ok(deleted) | ||
} | ||
} |
Oops, something went wrong.