├── .env.example ├── src ├── common │ ├── mod.rs │ └── error.rs ├── domain │ ├── mod.rs │ └── user │ │ ├── mod.rs │ │ ├── user_model.rs │ │ ├── user_repository.rs │ │ └── user_service.rs ├── presentation │ ├── mod.rs │ └── http │ │ ├── middleware │ │ ├── mod.rs │ │ └── service.rs │ │ ├── user │ │ ├── mod.rs │ │ ├── dto │ │ │ ├── mod.rs │ │ │ ├── user_response_dto.rs │ │ │ └── user_request_dto.rs │ │ └── user_controller.rs │ │ ├── mod.rs │ │ └── http_server.rs ├── configuration │ ├── mod.rs │ └── configuration.rs ├── data │ ├── infrastructure │ │ ├── mod.rs │ │ └── database.rs │ ├── repositories │ │ ├── mod.rs │ │ └── user │ │ │ ├── mod.rs │ │ │ ├── user_entity.rs │ │ │ ├── user_repository.rs │ │ │ └── user_data_store.rs │ └── mod.rs └── main.rs ├── migrations ├── 20221230175838_uuid-ossp.sql └── 20221230175909_users-table.sql ├── docker-compose.yml ├── Cargo.toml ├── .gitignore └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | PORT= -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error; -------------------------------------------------------------------------------- /src/domain/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user; -------------------------------------------------------------------------------- /src/presentation/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod http; -------------------------------------------------------------------------------- /src/configuration/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod configuration; -------------------------------------------------------------------------------- /src/data/infrastructure/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod database; -------------------------------------------------------------------------------- /src/data/repositories/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user; 2 | -------------------------------------------------------------------------------- /src/presentation/http/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod service; -------------------------------------------------------------------------------- /src/data/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod repositories; 2 | pub mod infrastructure; -------------------------------------------------------------------------------- /src/presentation/http/user/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user_controller; 2 | pub mod dto; -------------------------------------------------------------------------------- /src/presentation/http/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod http_server; 2 | pub mod user; 3 | mod middleware; -------------------------------------------------------------------------------- /src/domain/user/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user_model; 2 | pub mod user_repository; 3 | pub mod user_service; -------------------------------------------------------------------------------- /src/presentation/http/user/dto/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user_request_dto; 2 | pub mod user_response_dto; 3 | -------------------------------------------------------------------------------- /src/data/repositories/user/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user_data_store; 2 | pub mod user_entity; 3 | pub mod user_repository; -------------------------------------------------------------------------------- /migrations/20221230175838_uuid-ossp.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -------------------------------------------------------------------------------- /src/domain/user/user_model.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | 3 | #[derive(Debug)] 4 | pub struct User { 5 | pub user_id: Uuid, 6 | pub first_name: String, 7 | pub last_name: String, 8 | pub email: String, 9 | pub username: String, 10 | pub password: String, 11 | } -------------------------------------------------------------------------------- /src/presentation/http/middleware/service.rs: -------------------------------------------------------------------------------- 1 | use warp::Filter; 2 | 3 | pub fn with_service( 4 | service: T, 5 | ) -> impl Filter + Clone 6 | where 7 | T: Clone + Send, 8 | { 9 | warp::any().map(move || service.clone()) 10 | } -------------------------------------------------------------------------------- /src/presentation/http/user/dto/user_response_dto.rs: -------------------------------------------------------------------------------- 1 | use serde::{Serialize}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Serialize)] 5 | pub struct UserResponseDto { 6 | pub user_id: Uuid, 7 | pub first_name: String, 8 | pub last_name: String, 9 | pub email: String, 10 | pub username: String, 11 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | database: 5 | image: postgres 6 | ports: 7 | - '5432:5432' 8 | environment: 9 | POSTGRES_USER: postgres 10 | POSTGRES_PASSWORD: secret 11 | POSTGRES_DB: test 12 | restart: unless-stopped 13 | volumes: 14 | - rust-api-ddd:/var/lib/postgresql/data 15 | 16 | volumes: 17 | rust-api-ddd: ~ 18 | -------------------------------------------------------------------------------- /src/domain/user/user_repository.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use uuid::Uuid; 3 | use crate::domain::user::user_model::User; 4 | use crate::common::error::{Result, CustomError}; 5 | 6 | #[async_trait] 7 | pub trait IUserRepository { 8 | async fn create_user(&self, user: &User) -> Result; 9 | async fn list_users(&self) -> Result>; 10 | async fn get_user(&self, id: &Uuid) -> Result; 11 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-api-ddd" 3 | version = "0.1.0" 4 | authors = ["Botsaris Dimos "] 5 | edition = "2021" 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | async_once = "0.2.6" 10 | async-trait = "0.1.60" 11 | dotenv = "0.15.0" 12 | lazy_static = "1.4.0" 13 | pwhash = "1.0.0" 14 | serde = { version = "1.0.152", features = ["derive"] } 15 | serde_json = "1.0.91" 16 | sqlx = { version = "0.6.2", features = ["migrate", "postgres", "runtime-tokio-rustls", "uuid", "chrono"] } 17 | thiserror = "1.0.38" 18 | tokio = { version = "1.23.0", features = ["full"] } 19 | uuid = { version = "1.2.2", features = ["serde", "v4"] } 20 | warp = "0.3.3" 21 | -------------------------------------------------------------------------------- /migrations/20221230175909_users-table.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | CREATE TABLE IF NOT EXISTS users ( 3 | user_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 4 | first_name VARCHAR NOT NULL, 5 | last_name VARCHAR NOT NULL, 6 | username VARCHAR NOT NULL UNIQUE, 7 | email VARCHAR NOT NULL UNIQUE, 8 | password VARCHAR NOT NULL, 9 | created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP 11 | ); -------------------------------------------------------------------------------- /src/data/repositories/user/user_entity.rs: -------------------------------------------------------------------------------- 1 | use sqlx::types::chrono; 2 | use sqlx::FromRow; 3 | use uuid::Uuid; 4 | use crate::domain::user::user_model::User; 5 | 6 | #[derive(Debug, FromRow)] 7 | pub struct UserEntity { 8 | user_id: Uuid, 9 | first_name: String, 10 | last_name: String, 11 | email: String, 12 | username: String, 13 | password: String, 14 | created_at: chrono::DateTime, 15 | } 16 | 17 | impl From for User { 18 | fn from(entity: UserEntity) -> Self { 19 | Self { 20 | user_id: entity.user_id, 21 | first_name: entity.first_name, 22 | last_name: entity.last_name, 23 | email: entity.email, 24 | username: entity.username, 25 | password: entity.password, 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/presentation/http/user/dto/user_request_dto.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize}; 2 | use std::convert::TryFrom; 3 | use crate::domain::user::user_model::User; 4 | use thiserror::Error; 5 | use crate::common::error::CustomError; 6 | 7 | #[derive(Deserialize)] 8 | pub struct CreateUserDto { 9 | pub first_name: String, 10 | pub last_name: String, 11 | pub email: String, 12 | pub username: String, 13 | pub password: String, 14 | } 15 | 16 | 17 | impl TryFrom for User { 18 | type Error = CustomError; 19 | 20 | fn try_from(dto: CreateUserDto) -> Result { 21 | Ok(User { 22 | user_id: uuid::Uuid::nil(), 23 | first_name: dto.first_name, 24 | last_name: dto.last_name, 25 | email: dto.email, 26 | username: dto.username, 27 | password: dto.password 28 | }) 29 | } 30 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Application Related # 2 | ####################### 3 | .idea/ 4 | .vscode/ 5 | /target 6 | .env 7 | 8 | # Compiled source # 9 | ################### 10 | *.com 11 | *.class 12 | *.dll 13 | *.exe 14 | *.o 15 | *.so 16 | bundle 17 | 18 | # Packages # 19 | ############ 20 | # it's better to unpack these files and commit the raw source 21 | # git has its own built in compression methods 22 | *.7z 23 | *.dmg 24 | *.gz 25 | *.iso 26 | *.jar 27 | *.rar 28 | *.tar 29 | *.zip 30 | 31 | # Logs and databases # 32 | ###################### 33 | *.log 34 | *.sqlite 35 | 36 | # OS generated files # 37 | ###################### 38 | .DS_Store 39 | .DS_Store? 40 | ._* 41 | .Spotlight-V100 42 | .Trashes 43 | ehthumbs.db 44 | Thumbs.db 45 | 46 | # Generated by Cargo 47 | # will have compiled files and executables 48 | /target/ 49 | 50 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 51 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 52 | Cargo.lock 53 | 54 | # These are backup files generated by rustfmt 55 | **/*.rs.bk 56 | 57 | 58 | # Added by cargo 59 | 60 | /target 61 | -------------------------------------------------------------------------------- /src/common/error.rs: -------------------------------------------------------------------------------- 1 | use sqlx::Error as SqlxError; 2 | use thiserror::__private::AsDynError; 3 | use thiserror::Error as ThisError; 4 | use warp::Rejection; 5 | 6 | pub type Result = std::result::Result; 7 | 8 | #[derive(Clone, Debug, ThisError)] 9 | pub enum CustomError { 10 | #[error("An error occurred during database interaction. {0}")] 11 | DatabaseError(String), 12 | #[error("An error occurred during http interaction. {0}")] 13 | HttpError(String), 14 | } 15 | 16 | impl From for CustomError { 17 | fn from(sqlx_error: SqlxError) -> Self { 18 | match sqlx_error.as_database_error() { 19 | Some(db_error) => CustomError::DatabaseError(db_error.to_string()), 20 | None => { 21 | eprintln!("error {:?}", sqlx_error); 22 | CustomError::DatabaseError(String::from("Unrecognized database error!")) 23 | } 24 | } 25 | } 26 | } 27 | 28 | impl warp::reject::Reject for CustomError {} 29 | 30 | pub(crate) fn custom_reject(error: impl Into) -> Rejection { 31 | warp::reject::custom(CustomError::HttpError(String::from(error.into().to_string()))) 32 | } 33 | -------------------------------------------------------------------------------- /src/domain/user/user_service.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | use pwhash::bcrypt; 3 | 4 | use crate::domain::user::user_model::User; 5 | use crate::common::error::CustomError; 6 | use crate::domain::user::user_repository::IUserRepository; 7 | use crate::common::error::Result; 8 | 9 | pub struct UserService 10 | where 11 | R: IUserRepository, 12 | { 13 | user_repository: R, 14 | } 15 | 16 | impl UserService 17 | where 18 | R: IUserRepository, 19 | { 20 | pub fn new(user_repository: R) -> Self { 21 | Self { user_repository } 22 | } 23 | 24 | pub async fn create_user(&self, user: User) -> Result { 25 | let mut new_user = user; 26 | let password_hash = bcrypt::hash(new_user.password).unwrap(); 27 | new_user.password = password_hash; 28 | let user = self.user_repository.create_user(&new_user).await?; 29 | Ok(user) 30 | } 31 | 32 | pub async fn list_users(&self) -> Result> { 33 | let users =self.user_repository.list_users().await?; 34 | Ok(users) 35 | } 36 | 37 | pub async fn get_user(&self, user_id: &Uuid) -> Result { 38 | let user = self.user_repository.get_user(user_id).await?; 39 | Ok(user) 40 | } 41 | } -------------------------------------------------------------------------------- /src/data/infrastructure/database.rs: -------------------------------------------------------------------------------- 1 | use async_once::AsyncOnce; 2 | use lazy_static::lazy_static; 3 | use sqlx::pool::Pool; 4 | use sqlx::postgres::{PgPoolOptions, Postgres}; 5 | use std::env; 6 | 7 | use crate::common::error::CustomError; 8 | use crate::common::error::Result; 9 | use crate::Configuration; 10 | 11 | const DB_POOL_MAX_CONNECTIONS: u32 = 5; 12 | 13 | pub type DbPool = Pool; 14 | 15 | lazy_static! { 16 | static ref DB_POOL: AsyncOnce> = AsyncOnce::new(async { create_connection().await }); 17 | } 18 | 19 | pub async fn create_connection() -> Result> { 20 | let config = Configuration::new(); 21 | let db_uri = config.database.uri; 22 | 23 | PgPoolOptions::new() 24 | .max_connections(DB_POOL_MAX_CONNECTIONS) 25 | .connect(&db_uri) 26 | .await 27 | .map_err(CustomError::from) 28 | } 29 | 30 | pub async fn get_db_connection() -> Result { 31 | DB_POOL.get().await.clone() 32 | } 33 | 34 | pub async fn check_db_connection 35 | () -> Result<()> { 36 | println!("Checking on database connection..."); 37 | let pool = get_db_connection().await?; 38 | 39 | sqlx::query("SELECT 1") 40 | .fetch_one(&pool) 41 | .await 42 | .expect("Failed to PING database"); 43 | println!("Database PING executed successfully!"); 44 | 45 | Ok(()) 46 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::sync::Arc; 3 | use sqlx::migrate::Migrator; 4 | use crate::configuration::configuration::Configuration; 5 | use crate::data::infrastructure::database::{check_db_connection, get_db_connection}; 6 | use crate::data::repositories::user::user_data_store::UserDataStore; 7 | use crate::data::repositories::user::user_repository::UserRepository; 8 | use crate::domain::user::user_service::UserService; 9 | use crate::presentation::http::http_server::{Server, Services}; 10 | 11 | mod presentation; 12 | mod domain; 13 | mod common; 14 | mod data; 15 | mod configuration; 16 | 17 | static MIGRATOR: Migrator = sqlx::migrate!(); 18 | 19 | #[tokio::main] 20 | async fn main() -> std::io::Result<()> { 21 | let config = Configuration::new(); 22 | let db_pool = get_db_connection() 23 | .await 24 | .expect("Unable to connect to the database"); 25 | check_db_connection() 26 | .await 27 | .expect("Unable to ping the database"); 28 | MIGRATOR 29 | .run(&db_pool) 30 | .await 31 | .expect("Unable to run migrations"); 32 | 33 | let user_data_store = UserDataStore::new(db_pool.clone()); 34 | let user_repository = UserRepository::new(user_data_store); 35 | let user_service = Arc::new(UserService::new(user_repository)); 36 | 37 | let server = Server::new(config.server.port.parse().unwrap(), Services { user_service }); 38 | server.run().await; 39 | 40 | Ok(()) 41 | } -------------------------------------------------------------------------------- /src/data/repositories/user/user_repository.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use crate::common::error::CustomError; 3 | use crate::data::repositories::user::user_data_store::UserDataStore; 4 | use crate::data::repositories::user::user_entity::UserEntity; 5 | use crate::domain::user::user_model::User; 6 | use crate::domain::user::user_repository::IUserRepository; 7 | 8 | pub struct UserRepository { 9 | data_store: UserDataStore, 10 | } 11 | 12 | impl UserRepository { 13 | pub fn new(data_store: UserDataStore) -> Self { 14 | Self { data_store } 15 | } 16 | } 17 | 18 | #[async_trait] 19 | impl IUserRepository for UserRepository { 20 | async fn create_user(&self, user: &User) -> Result { 21 | let result: UserEntity = self 22 | .data_store 23 | .create_user(user) 24 | .await?; 25 | 26 | Ok(User::from(result)) 27 | } 28 | 29 | async fn list_users(&self) -> Result, CustomError> { 30 | let result: Vec = self 31 | .data_store 32 | .list_users() 33 | .await?; 34 | 35 | Ok(result.into_iter().map(User::from).collect()) 36 | } 37 | 38 | async fn get_user(&self, user_id: &uuid::Uuid) -> Result { 39 | let result: UserEntity = self 40 | .data_store 41 | .get_user(user_id) 42 | .await?; 43 | 44 | Ok(User::from(result)) 45 | } 46 | } -------------------------------------------------------------------------------- /src/data/repositories/user/user_data_store.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use uuid::Uuid; 3 | use crate::common::error::{CustomError, Result}; 4 | use crate::data::infrastructure::database::DbPool; 5 | use crate::data::repositories::user::user_entity::UserEntity; 6 | use crate::domain::user::user_model::User; 7 | 8 | pub struct UserDataStore { 9 | db_pool: DbPool, 10 | } 11 | 12 | impl UserDataStore { 13 | pub fn new(db_pool: DbPool) -> Self { 14 | Self { db_pool } 15 | } 16 | } 17 | 18 | impl UserDataStore { 19 | pub async fn create_user(&self, user: &User) -> Result { 20 | let query = r#" 21 | INSERT INTO users (first_name, last_name, email, username, password) 22 | VALUES ($1, $2, $3, $4, $5) 23 | RETURNING * 24 | "#; 25 | let result: UserEntity = sqlx::query_as::<_, UserEntity>(query) 26 | .bind(&user.first_name) 27 | .bind(&user.last_name) 28 | .bind(&user.email) 29 | .bind(&user.username) 30 | .bind(&user.password) 31 | .fetch_one(&self.db_pool) 32 | .await?; 33 | 34 | Ok(result) 35 | } 36 | 37 | pub async fn list_users(&self) -> Result> { 38 | let query = r#"SELECT * FROM users"#; 39 | let result: Vec = sqlx::query_as::<_, UserEntity>(query) 40 | .fetch_all(&self.db_pool) 41 | .await?; 42 | 43 | Ok(result) 44 | } 45 | 46 | pub async fn get_user(&self, user_id: &Uuid) -> Result { 47 | let query = r#"SELECT * FROM users WHERE user_id = $1"#; 48 | let result: UserEntity = sqlx::query_as::<_, UserEntity>(query) 49 | .bind(&user_id) 50 | .fetch_one(&self.db_pool) 51 | .await?; 52 | 53 | Ok(result) 54 | } 55 | } -------------------------------------------------------------------------------- /src/configuration/configuration.rs: -------------------------------------------------------------------------------- 1 | use dotenv::dotenv; 2 | use std::env; 3 | use lazy_static::lazy_static; 4 | 5 | lazy_static! { 6 | static ref CONFIGURATION: Configuration = { 7 | Configuration::new() 8 | }; 9 | } 10 | 11 | pub fn get_configurations() -> &'static Configuration { 12 | &CONFIGURATION 13 | } 14 | 15 | pub struct DatabaseConfiguration { 16 | pub uri: String, 17 | } 18 | 19 | pub struct ServerConfiguration { 20 | pub port: String, 21 | } 22 | 23 | pub struct Configuration { 24 | pub database: DatabaseConfiguration, 25 | pub server: ServerConfiguration, 26 | } 27 | 28 | impl Default for DatabaseConfiguration { 29 | fn default() -> Self { 30 | DatabaseConfiguration { 31 | uri: env::var("DATABASE_URL").expect("Missing \"DATABASE_URL\" environment variable"), 32 | } 33 | } 34 | } 35 | 36 | impl Default for ServerConfiguration { 37 | fn default() -> Self { 38 | ServerConfiguration { 39 | port: env::var("PORT").expect("PORT must be set"), 40 | } 41 | } 42 | } 43 | 44 | impl DatabaseConfiguration { 45 | pub fn new() -> Self { 46 | DatabaseConfiguration::default() 47 | } 48 | } 49 | 50 | impl ServerConfiguration { 51 | pub fn new() -> Self { 52 | ServerConfiguration::default() 53 | } 54 | } 55 | 56 | impl Default for Configuration { 57 | fn default() -> Configuration { 58 | Configuration { 59 | database: DatabaseConfiguration::new(), 60 | server: ServerConfiguration::new(), 61 | } 62 | } 63 | } 64 | 65 | impl Configuration { 66 | pub fn new() -> Self { 67 | dotenv() 68 | .ok() 69 | .expect("Unable to find .env file. Create one based on the .env.example"); 70 | Configuration::default() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/presentation/http/http_server.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | use uuid::Uuid; 3 | use warp::{http, path}; 4 | use warp::Filter; 5 | use warp::filters::BoxedFilter; 6 | use crate::domain::user::user_service::UserService; 7 | use crate::UserRepository; 8 | 9 | use super::user::user_controller; 10 | use super::middleware; 11 | 12 | pub struct Services { 13 | pub user_service: Arc>, 14 | } 15 | 16 | pub struct Server { 17 | port: u16, 18 | services: Services, 19 | } 20 | 21 | fn path_users_prefix() -> BoxedFilter<()> { 22 | path!("v1" / "users" / ..).boxed() 23 | } 24 | 25 | impl Server { 26 | pub fn new(port: u16, services: Services) -> Self { 27 | Self { port, services } 28 | } 29 | 30 | pub async fn run(&self) { 31 | let cors = warp::cors() 32 | .allow_any_origin() 33 | .allow_credentials(true) 34 | .allow_headers(vec![ 35 | http::header::AUTHORIZATION, 36 | http::header::CONTENT_TYPE, 37 | ]) 38 | .allow_methods(&[ 39 | http::Method::GET, 40 | http::Method::OPTIONS, 41 | http::Method::POST, 42 | http::Method::PUT, 43 | ]); 44 | 45 | let list_users = warp::get() 46 | .and(path_users_prefix()) 47 | .and(path::end()) 48 | .and(middleware::service::with_service(self.services.user_service.clone())) 49 | .and_then(user_controller::list_users); 50 | 51 | let create_user = warp::post() 52 | .and(path_users_prefix()) 53 | .and(path::end()) 54 | .and(middleware::service::with_service(self.services.user_service.clone())) 55 | .and(warp::body::json()) 56 | .and_then(user_controller::create_user); 57 | 58 | 59 | let get_user = warp::get() 60 | .and(path_users_prefix()) 61 | .and(middleware::service::with_service(self.services.user_service.clone())) 62 | .and(path::param::()) 63 | .and_then(user_controller::get_user); 64 | 65 | let user_routes = create_user.or(list_users).or(get_user); 66 | 67 | warp::serve(user_routes.with(cors)) 68 | .run(([127, 0, 0, 1], self.port)) 69 | .await; 70 | } 71 | } -------------------------------------------------------------------------------- /src/presentation/http/user/user_controller.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::sync::Arc; 3 | use uuid::Uuid; 4 | use warp::reply::json; 5 | use warp::{Error, reject, Rejection, Reply}; 6 | use crate::domain::user::user_model::User; 7 | use crate::domain::user::user_service::UserService; 8 | use crate::presentation::http::user::dto::user_request_dto::CreateUserDto; 9 | use crate::presentation::http::user::dto::user_response_dto::UserResponseDto; 10 | use crate::UserRepository; 11 | 12 | pub async fn list_users( 13 | user_service: Arc>, 14 | ) -> Result { 15 | match user_service.list_users().await { 16 | Ok(users) => Ok(json( 17 | &users 18 | .into_iter() 19 | .map(|u| UserResponseDto { 20 | user_id: u.user_id, 21 | first_name: u.first_name, 22 | last_name: u.last_name, 23 | email: u.email, 24 | username: u.username, 25 | }) 26 | .collect::>(), 27 | )), 28 | Err(e) => { 29 | Err(reject()) 30 | } 31 | } 32 | } 33 | 34 | pub async fn create_user( 35 | user_service: Arc>, 36 | body: CreateUserDto, 37 | ) -> Result { 38 | let new_user = User::try_from(body).map_err(reject::custom)?; 39 | match user_service 40 | .create_user(new_user) 41 | .await 42 | { 43 | Ok(created_user) => Ok(json(&UserResponseDto { 44 | user_id: created_user.user_id, 45 | first_name: created_user.first_name, 46 | last_name: created_user.last_name, 47 | email: created_user.email, 48 | username: created_user.username, 49 | })), 50 | Err(e) => { 51 | Err(reject()) 52 | } 53 | } 54 | } 55 | 56 | pub async fn get_user( 57 | user_service: Arc>, 58 | id: Uuid, 59 | ) -> Result { 60 | match user_service 61 | .get_user(&id) 62 | .await 63 | { 64 | Ok(user) => Ok(json(&UserResponseDto { 65 | user_id: user.user_id, 66 | first_name: user.first_name, 67 | last_name: user.last_name, 68 | email: user.email, 69 | username: user.username, 70 | })), 71 | Err(e) => { 72 | Err(reject()) 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-api-ddd 2 | 3 | # What is this repository for? # 4 | Rust server architecture showcase using [Warp](https://github.com/seanmonstar/warp) and [Sqlx](https://github.com/launchbadge/sqlx) for Postgres queries. 5 | 6 | # Architecture Overview # 7 | The app is designed to use a layered architecture. The architecture is heavily influenced by the Clean Architecture.[Clean Architecture](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html) is an architecture where: 8 | 9 | 1. **does not depend on the existence of some framework, database, external agency.** 10 | 2. **does not depend on UI** 11 | 3. **the business rules can be tested without the UI, database, web server, or any external element.** 12 | 13 |

14 | 15 | 16 |

17 | 18 |

19 | 20 | 21 |

22 | 23 | Also, in entry point(main.rs), I use Dependency Injection(DI). There are many reasons using Dependency Injection as: 24 | 1. Decoupling 25 | 2. Easier unit testing 26 | 3. Faster development 27 | 4. Dependency injection is really helpful when it comes to testing. You can easily mock your modules' dependencies using this pattern. 28 | 29 | ## Data Layer ## 30 | 31 | The data layer is implemented using repositories, that hide the underlying data sources (database, network, cache, etc), and provides an abstraction over them so other parts of the application that make use of the repositories, don't care about the origin of the data and are decoupled from the specific implementations used. Furthermore, the repositories are responsible to map the entities they fetch from the data sources to the models used in the applications. This is important to enable the decoupling. 32 | 33 | ## Domain Layer ## 34 | 35 | The domain layer is implemented using services. They depend on the repositories to get the app models and apply the business rules on them. They are not coupled to a specific database implementation and can be reused if we add more data sources to the app or even if we change the database for example from MongoDB to Couchbase Server. 36 | 37 | ## Routes/Controller Layer ## 38 | 39 | This layer is being used in the express app and depends on the domain layer (services). Here we define the routes that can be called from outside. The services are always used as the last middleware on the routes. That means that the middlewares registered before should not alter data being passed to the domain layer. They are only allowed to act upon the data without modification, like for example validating the data. 40 | 41 | # Quick start # 42 | 43 | ### Prerequisites ### 44 | 45 | Create an .env file in project root to register the following required environment variables: 46 | - `DATABASE_URL` - Postgres connection URL 47 | - `PORT` - port of server 48 | 49 | ### Use Docker: ### 50 | 51 | You can use Docker to start the app locally. The Dockerfile and the docker-compose.yml are already provided for you. You have to run the following command: 52 | 53 | ```shell 54 | docker-compose up 55 | ``` 56 | 57 | and then run: 58 | 59 | ```shell 60 | cargo run 61 | ``` 62 | 63 | ## Support Me 64 | 65 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Y8Y797KCA) 66 | 67 | ## Show your support 68 | 69 | Give a ⭐️ if this project helped you! 70 | --------------------------------------------------------------------------------