├── .env.sample ├── .gitignore ├── Cargo.toml ├── README.md ├── diesel.toml ├── docker-compose.yml ├── environment └── local │ └── Dockerfile.mysql ├── migrations ├── .gitkeep └── 2021-01-10-130452_create_documents │ ├── down.sql │ └── up.sql └── src ├── domains ├── documents.rs └── mod.rs ├── infrastructures ├── database │ ├── mod.rs │ └── schema.rs ├── mod.rs └── repository │ ├── documents.rs │ └── mod.rs ├── main.rs ├── server ├── handlers.rs ├── mod.rs ├── request.rs └── response.rs └── usecases ├── documents.rs └── mod.rs /.env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sample-api" 3 | version = "0.1.0" 4 | authors = ["kz_morita "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | actix-web = "3" 11 | futures = "0.3.9" 12 | serde = "1.0.118" 13 | serde_derive = "1.0.118" 14 | serde_json = "1.0.61" 15 | dotenv = "0.15.0" 16 | chrono = "0.4" 17 | failure = "0.1.8" 18 | 19 | [dependencies.diesel] 20 | features = ["mysql", "chrono", "r2d2"] 21 | version = "1.4.5" 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust API Server Architecture Sample 2 | 3 | This is sample code of layered architecture in rust. 4 | 5 | 6 | ## Layer 7 | 8 | - domains 9 | - Contains domain logic 10 | - infrastructures 11 | - To access external resources such as Database and HTTP Requests 12 | - usecases 13 | - Call the domain logic and make adjustments to meet the required use cases. 14 | - server 15 | - Interface such as HTTP Server . 16 | 17 | -------------------------------------------------------------------------------- /diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/infrastructures/database/schema.rs" 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | sample_mysql: 5 | build: 6 | context: . 7 | dockerfile: environment/local/Dockerfile.mysql 8 | image: sample_mysql 9 | command: mysqld --character-set-server=utf8mb4 --character-set-filesystem=utf8mb4 10 | ports: 11 | - "13306:3306" 12 | volumes: 13 | - sample_mysql:/var/lib/mysql 14 | environment: 15 | MYSQL_DATABASE: sample 16 | MYSQL_ROOT_PASSWORD: root 17 | MYSQL_USER: sample 18 | MYSQL_PASSWORD: sample 19 | networks: 20 | - sample 21 | 22 | volumes: 23 | sample_mysql: 24 | networks: 25 | sample: 26 | -------------------------------------------------------------------------------- /environment/local/Dockerfile.mysql: -------------------------------------------------------------------------------- 1 | FROM mysql:8.0.22 2 | -------------------------------------------------------------------------------- /migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/foresta/rust-api-architecture-sample/69c34a6b8b4bfa46bc1c49b550e82a53e627cf2b/migrations/.gitkeep -------------------------------------------------------------------------------- /migrations/2021-01-10-130452_create_documents/down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE documents; 2 | -------------------------------------------------------------------------------- /migrations/2021-01-10-130452_create_documents/up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE documents ( 2 | id bigint unsigned NOT NULL AUTO_INCREMENT, 3 | title varchar(500) NOT NULL, 4 | body text NOT NULL, 5 | PRIMARY KEY(`id`) 6 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 7 | -------------------------------------------------------------------------------- /src/domains/documents.rs: -------------------------------------------------------------------------------- 1 | use super::Id; 2 | use failure::Error; 3 | 4 | pub type DocumentId = Id; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct Document { 8 | pub id: DocumentId, 9 | pub title: String, 10 | pub body: String, 11 | } 12 | 13 | impl Document { 14 | pub fn create(title: String, body: String) -> Self { 15 | Self { 16 | id: Default::default(), 17 | title: title, 18 | body: body, 19 | } 20 | } 21 | } 22 | 23 | pub trait DocumentRepository { 24 | fn find_by_id(&self, document_id: DocumentId) -> Result; 25 | fn list(&self) -> Result, Error>; 26 | fn insert(&self, document: &Document) -> Result<(), Error>; 27 | fn update(&self, document: &Document) -> Result<(), Error>; 28 | fn delete(&self, document: &Document) -> Result<(), Error>; 29 | } 30 | -------------------------------------------------------------------------------- /src/domains/mod.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | pub mod documents; 4 | 5 | #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Ord, PartialOrd)] 6 | pub struct Id { 7 | id: u64, 8 | _phantom: PhantomData, 9 | } 10 | 11 | impl Id { 12 | pub fn new(id: u64) -> Self { 13 | Self { 14 | id, 15 | _phantom: PhantomData, 16 | } 17 | } 18 | 19 | pub fn get(&self) -> u64 { 20 | self.id 21 | } 22 | } 23 | 24 | impl Default for Id { 25 | fn default() -> Self { 26 | Id::new(0) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/infrastructures/database/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod schema; 2 | -------------------------------------------------------------------------------- /src/infrastructures/database/schema.rs: -------------------------------------------------------------------------------- 1 | table! { 2 | documents (id) { 3 | id -> Unsigned, 4 | title -> Varchar, 5 | body -> Text, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/infrastructures/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod database; 2 | pub mod repository; 3 | -------------------------------------------------------------------------------- /src/infrastructures/repository/documents.rs: -------------------------------------------------------------------------------- 1 | use super::super::database::schema::*; 2 | use crate::domains::documents::{Document, DocumentId, DocumentRepository}; 3 | use diesel::prelude::*; 4 | use diesel::r2d2::{ConnectionManager, Pool}; 5 | use failure::Error; 6 | 7 | // 8 | // Entity 9 | // 10 | 11 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Insertable)] 12 | #[table_name = "documents"] 13 | pub struct NewDocumentEntity { 14 | pub title: String, 15 | pub body: String, 16 | } 17 | 18 | impl NewDocumentEntity { 19 | fn from(model: &Document) -> NewDocumentEntity { 20 | NewDocumentEntity { 21 | title: model.title.to_owned(), 22 | body: model.body.to_owned(), 23 | } 24 | } 25 | } 26 | 27 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Queryable, Identifiable, AsChangeset)] 28 | #[table_name = "documents"] 29 | pub struct DocumentEntity { 30 | pub id: u64, 31 | pub title: String, 32 | pub body: String, 33 | } 34 | 35 | impl DocumentEntity { 36 | fn from(model: &Document) -> DocumentEntity { 37 | DocumentEntity { 38 | id: model.id.get(), 39 | title: model.title.to_owned(), 40 | body: model.body.to_owned(), 41 | } 42 | } 43 | 44 | fn of(&self) -> Document { 45 | Document { 46 | id: DocumentId::new(self.id), 47 | title: self.title.to_owned(), 48 | body: self.body.to_owned(), 49 | } 50 | } 51 | } 52 | 53 | pub struct DocumentRepositoryImpl { 54 | pub pool: Box>>, 55 | } 56 | 57 | impl DocumentRepository for DocumentRepositoryImpl { 58 | fn find_by_id(&self, document_id: DocumentId) -> Result { 59 | use super::super::database::schema::documents::dsl; 60 | 61 | let conn = self.pool.get()?; 62 | let entity: DocumentEntity = dsl::documents 63 | .filter(documents::id.eq(document_id.get())) 64 | .get_result(&conn)?; 65 | 66 | Ok(entity.of()) 67 | } 68 | 69 | fn list(&self) -> Result, Error> { 70 | use super::super::database::schema::documents::dsl; 71 | 72 | let query = dsl::documents.into_boxed(); 73 | let conn = self.pool.get()?; 74 | let results: Vec = query.limit(100).load(&conn)?; 75 | 76 | Ok(results.into_iter().map(|e| e.of()).collect()) 77 | } 78 | 79 | fn insert(&self, document: &Document) -> Result<(), Error> { 80 | use super::super::database::schema::documents::dsl; 81 | 82 | let entity = NewDocumentEntity::from(document); 83 | let conn = self.pool.get().unwrap(); 84 | diesel::insert_into(dsl::documents) 85 | .values(entity) 86 | .execute(&conn)?; 87 | 88 | Ok(()) 89 | } 90 | 91 | fn update(&self, document: &Document) -> Result<(), Error> { 92 | let entity = DocumentEntity::from(document); 93 | let conn = self.pool.get().unwrap(); 94 | diesel::update(documents::table) 95 | .set(&entity) 96 | .execute(&conn)?; 97 | 98 | Ok(()) 99 | } 100 | 101 | fn delete(&self, document: &Document) -> Result<(), Error> { 102 | let entity = DocumentEntity::from(document); 103 | let conn = self.pool.get().unwrap(); 104 | diesel::delete(&entity).execute(&conn)?; 105 | 106 | Ok(()) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/infrastructures/repository/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod documents; 2 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate diesel; 3 | 4 | mod domains; 5 | mod infrastructures; 6 | mod server; 7 | mod usecases; 8 | 9 | fn main() -> std::io::Result<()> { 10 | server::run() 11 | } 12 | -------------------------------------------------------------------------------- /src/server/handlers.rs: -------------------------------------------------------------------------------- 1 | use super::request::*; 2 | use super::response::*; 3 | use super::RequestContext; 4 | use crate::domains::documents::DocumentId; 5 | use crate::usecases; 6 | use actix_web::{delete, get, post, put, web, web::Json, HttpResponse, Responder}; 7 | 8 | #[post("/documents")] 9 | async fn post_document( 10 | data: web::Data, 11 | request: Json, 12 | ) -> impl Responder { 13 | match usecases::documents::post_document(data.document_repository(), &request.of()) { 14 | Ok(_) => HttpResponse::NoContent().finish(), 15 | Err(_) => HttpResponse::InternalServerError().json(""), 16 | } 17 | } 18 | 19 | #[put("/documents/{id}")] 20 | async fn update_document( 21 | data: web::Data, 22 | path_params: web::Path<(u32,)>, 23 | request: Json, 24 | ) -> impl Responder { 25 | let document_id = DocumentId::new(path_params.into_inner().0.into()); 26 | let document = request.model(document_id); 27 | match usecases::documents::update_document(data.document_repository(), &document) { 28 | Ok(_) => HttpResponse::NoContent().finish(), 29 | Err(_) => HttpResponse::InternalServerError().json(""), 30 | } 31 | } 32 | 33 | #[delete("/documents/{id}")] 34 | async fn delete_document( 35 | data: web::Data, 36 | path_params: web::Path<(u32,)>, 37 | ) -> impl Responder { 38 | let document_id = DocumentId::new(path_params.into_inner().0.into()); 39 | match usecases::documents::delete_document(data.document_repository(), document_id) { 40 | Ok(_) => HttpResponse::NoContent().finish(), 41 | Err(_) => HttpResponse::InternalServerError().json(""), 42 | } 43 | } 44 | 45 | #[get("/documents/{id}")] 46 | async fn get_document( 47 | data: web::Data, 48 | path_params: web::Path<(u32,)>, 49 | ) -> impl Responder { 50 | let document_id = DocumentId::new(path_params.into_inner().0.into()); 51 | match usecases::documents::get_document(data.document_repository(), document_id) { 52 | Ok(document) => HttpResponse::Ok().json(DocumentDto::new(&document)), 53 | Err(_) => HttpResponse::InternalServerError().json(""), 54 | } 55 | } 56 | 57 | #[get("/documents")] 58 | async fn get_documents(data: web::Data) -> impl Responder { 59 | match usecases::documents::get_document_list(data.document_repository()) { 60 | Ok(documents) => HttpResponse::Ok().json(DocumentListResponse::new(documents)), 61 | Err(_) => HttpResponse::InternalServerError().json(""), 62 | } 63 | } 64 | 65 | #[get("/")] 66 | async fn hello() -> impl Responder { 67 | HttpResponse::Ok().body("Hello world!") 68 | } 69 | -------------------------------------------------------------------------------- /src/server/mod.rs: -------------------------------------------------------------------------------- 1 | mod handlers; 2 | mod request; 3 | mod response; 4 | 5 | use crate::domains::documents::DocumentRepository; 6 | use actix_web::{App, HttpServer}; 7 | use diesel::prelude::*; 8 | use diesel::r2d2::{ConnectionManager, Pool}; 9 | use dotenv::dotenv; 10 | use std::env; 11 | 12 | #[actix_web::main] 13 | pub async fn run() -> std::io::Result<()> { 14 | HttpServer::new(|| { 15 | App::new() 16 | .data(RequestContext::new()) 17 | .service(handlers::hello) 18 | .service(handlers::get_documents) 19 | .service(handlers::get_document) 20 | .service(handlers::post_document) 21 | .service(handlers::delete_document) 22 | .service(handlers::update_document) 23 | }) 24 | .bind("127.0.0.1:8080")? 25 | .run() 26 | .await 27 | } 28 | 29 | #[derive(Clone)] 30 | pub struct RequestContext { 31 | pool: Pool>, 32 | } 33 | 34 | impl RequestContext { 35 | pub fn new() -> RequestContext { 36 | dotenv().ok(); 37 | let database_url = env::var("DATABASE_URL").expect("DATABASE_URL is not set"); 38 | let manager = ConnectionManager::::new(database_url); 39 | let pool = Pool::builder() 40 | .build(manager) 41 | .expect("Failed to create DB connection pool."); 42 | 43 | RequestContext { pool } 44 | } 45 | 46 | pub fn document_repository(&self) -> impl DocumentRepository { 47 | use crate::infrastructures::repository::documents::DocumentRepositoryImpl; 48 | 49 | DocumentRepositoryImpl { 50 | pool: Box::new(self.pool.to_owned()), 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/server/request.rs: -------------------------------------------------------------------------------- 1 | use crate::domains::documents::{Document, DocumentId}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Default, Deserialize, Serialize)] 5 | pub struct DocumentRequest { 6 | title: String, 7 | body: String, 8 | } 9 | 10 | impl DocumentRequest { 11 | pub fn of(&self) -> Document { 12 | Document::create(self.title.to_owned(), self.body.to_owned()) 13 | } 14 | 15 | pub fn model(&self, document_id: DocumentId) -> Document { 16 | Document { 17 | id: document_id, 18 | title: self.title.to_owned(), 19 | body: self.body.to_owned(), 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/server/response.rs: -------------------------------------------------------------------------------- 1 | use crate::domains::documents::Document; 2 | use serde::Serialize; 3 | 4 | #[derive(Debug, Clone, Serialize)] 5 | pub struct DocumentListResponse { 6 | documents: Vec, 7 | } 8 | 9 | impl DocumentListResponse { 10 | pub fn new(docs: Vec) -> DocumentListResponse { 11 | DocumentListResponse { 12 | documents: docs.iter().map(|d| DocumentDto::new(&d)).collect(), 13 | } 14 | } 15 | } 16 | 17 | #[derive(Debug, Clone, Serialize)] 18 | pub struct DocumentDto { 19 | id: u64, 20 | title: String, 21 | body: String, 22 | } 23 | 24 | impl DocumentDto { 25 | pub fn new(model: &Document) -> DocumentDto { 26 | DocumentDto { 27 | id: model.id.get(), 28 | title: model.title.to_owned(), 29 | body: model.body.to_owned(), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/usecases/documents.rs: -------------------------------------------------------------------------------- 1 | use crate::domains::documents::{Document, DocumentId, DocumentRepository}; 2 | use failure::Error; 3 | 4 | pub fn get_document_list(repository: impl DocumentRepository) -> Result, Error> { 5 | repository.list() 6 | } 7 | 8 | pub fn get_document( 9 | repository: impl DocumentRepository, 10 | document_id: DocumentId, 11 | ) -> Result { 12 | repository.find_by_id(document_id) 13 | } 14 | 15 | pub fn post_document( 16 | repository: impl DocumentRepository, 17 | document: &Document, 18 | ) -> Result<(), Error> { 19 | repository.insert(document) 20 | } 21 | 22 | pub fn update_document( 23 | repository: impl DocumentRepository, 24 | document: &Document, 25 | ) -> Result<(), Error> { 26 | repository.update(document) 27 | } 28 | 29 | pub fn delete_document( 30 | repository: impl DocumentRepository, 31 | document_id: DocumentId, 32 | ) -> Result<(), Error> { 33 | let document = repository.find_by_id(document_id)?; 34 | repository.delete(&document) 35 | } 36 | -------------------------------------------------------------------------------- /src/usecases/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod documents; 2 | --------------------------------------------------------------------------------