## `axum-restful` A restful framework based on `axum` and `sea-orm`. Inspired by `django-rest-framework`. The goal of the project is to build an enterprise-level production framework. ## Features - a Trait for the `struct` generated by `sea-orm` to provide with GET, PUT, DELETE methods - `tls` support - `prometheus` metrics and metrics server - `graceful shutdown`support - `swagger document` generate based on [`aide`](https://github.com/tamasfe/aide) ## Quick start A full example is exists at `axum-restful/examples/demo`. First, you can create a new crate like `cargo new axum-restful-demo`. #### Build a database service You should have a database service before. It is recommended to use `postgresql` database. you can use docker and docker compose to start a `postgresql` create a `compose.yaml` in the same directory as `Cargo.toml` ```yaml services: postgres: image: postgres:15-bullseye container_name: demo-postgres restart: always volumes: - demo-postgres:/var/lib/postgresql/data ports: - "127.0.0.1:5432:5432" environment: - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} volumes: demo-postgres: {} ``` a `.env`file like ``` # config the base pg connect params POSTGRES_DB=demo POSTGRES_USER=demo-user POSTGRES_PASSWORD=demo-password # used by axum-restful framework to specific a database connection DATABASE_URL=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB} ``` finally, you can build a service with `docker compose up -d` #### Write and migrate a migration For more details, please refer to the [`sea-orm`](https://www.sea-ql.org/SeaORM/docs/index/) documentation. Install the `sea-orm-cli` with `cargo` ```shell $ cargo install sea-orm-cli ``` Configure dependencies and workspace in `Cargo.toml` ```toml [package] name = "demo" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [workspace] members = [".", "migration"] [dependencies] aide = "0.13" axum = "0.7" axum-restful = "0.5" chrono = "0.4" migration = { path = "./migration" } once_cell = "1" schemars = { version = "0.8", features = ["chrono"] } sea-orm = { version = "0.12", features = ["macros", "sqlx-postgres", "runtime-tokio-rustls"] } sea-orm-migration = { version = "0.12", features = ["sqlx-postgres", "runtime-tokio-rustls",] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tokio = { version = "1", features = ["full"] } tracing = "0.1" tracing-subscriber = "0.3" ``` Setup the migration directory in `./migration` ```shell $ sea-orm-cli migrate init ``` project structure changed into ``` ├── Cargo.lock ├── Cargo.toml ├── compose.yaml ├── migration │   ├── Cargo.toml │   ├── README.md │   └── src │   ├── lib.rs │   ├── m20220101_000001_create_table.rs │   └── main.rs └── src └── main.rs ``` edit the `m20****_******_create_table.rs` file blow `./migration/src` ```rust use sea_orm_migration::prelude::*; #[derive(DeriveMigrationName)] pub struct Migration; #[async_trait::async_trait] impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { // Replace the sample below with your own migration scripts manager .create_table( Table::create() .table(Student::Table) .if_not_exists() .col( ColumnDef::new(Student::Id) .big_integer() .not_null() .auto_increment() .primary_key(), ) .col(ColumnDef::new(Student::Name).string().not_null()) .col(ColumnDef::new(Student::Region).string().not_null()) .col(ColumnDef::new(Student::Age).small_integer().not_null()) .col(ColumnDef::new(Student::CreateTime).date_time().not_null()) .col(ColumnDef::new(Student::Score).double().not_null()) .col( ColumnDef::new(Student::Gender) .boolean() .not_null() .default(Expr::value(true)), ) .to_owned(), ) .await } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { // Replace the sample below with your own migration scripts manager .drop_table(Table::drop().table(Student::Table).to_owned()) .await } } /// Learn more at https://docs.rs/sea-query#iden #[derive(Iden)] enum Student { Table, Id, Name, Region, Age, CreateTime, Score, Gender, } ``` edit `migration/Cargo.toml` to add dependencies ```toml [dependencies] ... axum-restful = "0.5" ``` edit `migration/src/main.rs` to specific a database connection an migrate ```rust use sea_orm_migration::prelude::*; #[async_std::main] async fn main() { // cli::run_cli(migration::Migrator).await; let db = axum_restful::get_db_connection_pool().await; migration::Migrator::up(db, None).await.unwrap(); } ``` migrate the migration files ```shell $ cd migration $ cargo run ``` finally, you can see two tables named `sql_migrations` and `student`generated. #### Generate entities at the project root path ```shell $ sea-orm-cli generate entity -o src/entities ``` will generate entities configure and code, now project structure changed into ``` ├── Cargo.lock ├── Cargo.toml ├── compose.yaml ├── migration │   ├── Cargo.toml │   ├── README.md │   └── src │   ├── lib.rs │   ├── m20220101_000001_create_table.rs │   └── main.rs └── src ├── entities │   ├── mod.rs │   ├── prelude.rs │   └── student.rs └── main.rs ``` edit the `src/entities/student.rs` to add derive `Default, Serialize, Deserialize` ```rust //! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.0 use schemars::JsonSchema; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, JsonSchema, DeriveEntityModel, Serialize, Deserialize)] #[sea_orm(table_name = "student")] pub struct Model { #[sea_orm(primary_key)] pub id: i64, pub name: String, pub region: String, pub age: i16, pub create_time: DateTime, #[sea_orm(column_type = "Double")] pub score: f64, pub gender: bool, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} impl ActiveModelBehavior for ActiveModel {} ``` edit `src/main.rs` ```rust use schemars::JsonSchema; use sea_orm_migration::prelude::MigratorTrait; use tokio::net::TcpListener; use axum_restful::swagger::SwaggerGeneratorExt; use axum_restful::views::ModelViewExt; use crate::entities::student; mod check; mod entities; #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); let db = axum_restful::get_db_connection_pool().await; let _ = migration::Migrator::down(db, None).await; migration::Migrator::up(db, None).await.unwrap(); tracing::info!("migrate success"); aide::gen::on_error(|error| { tracing::error!("swagger api gen error: {error}"); }); aide::gen::extract_schemas(true); /// student #[derive(JsonSchema)] struct StudentView; impl ModelViewExt for StudentView { fn order_by_desc() -> student::Column { student::Column::Id } } let path = "/api/student"; let app = StudentView::http_router(path); check::check_curd_operate_correct(app.clone(), path, db).await; // if you want to generate swagger docs // impl OperationInput and SwaggerGenerator and change app into http_routers_with_swagger impl aide::operation::OperationInput for student::Model {} impl axum_restful::swagger::SwaggerGeneratorExt for StudentView {} let app = StudentView::http_router_with_swagger(path, StudentView::model_api_router()).await.unwrap(); let addr = "0.0.0.0:3000"; tracing::info!("listen at {addr}"); tracing::info!("visit http://127.0.0.1:3000/docs/swagger/ for swagger api"); let listener = TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app.into_make_service()).await.unwrap(); } ``` `StudentView impl the ModelView`, the `T` is `student::ActiveModel` that represent the `student table configure` in the database, if will has full HTTP methods with GET, POST, PUT, DELETE. you can see the server is listen at port 3000 #### Verify the service #### Swagger if you `impl axum_restful::swagger::SwaggerGenerator` above, then you can visit `http://127.0.0.1:3000/docs/swagger/` at your browser, you will see a swagger document is generated ![swagger-ui](https://github.com/gongzhengyang/axum-restful/blob/main/statics/swagger-ui-demo.png) ## License Licensed under either of - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) - MIT license ([LICENSE-MIT](LICENSE-MIT) or ) at your option. ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.