
Hei, hari ini saya ingin berbagi pengetahuan saya tentang cara menulis API istirahat di Rust. Mungkin lebih mudah dari yang Anda pikirkan! Kami tidak akan menampilkan konektivitas basis data dalam artikel ini. Sebaliknya, kami fokus pada menunjukkan cara menghasilkan spesifikasi OpenAPI dan melayani Swagger UI .
Anda dapat menemukan sumber kode lengkap di GitHub.
Sebelum memulai, pastikan Anda telah dipasang karat.
Mari kita mulai dengan menginisialisasi proyek baru menggunakan cargo init .
cargo init my-rest-api
cd my-rest-apiYang seharusnya menghasilkan struktur direktori berikut:
├── Cargo.toml
└── src
└── main.rs Anda dapat menggunakan rustfmt untuk memformat. Untuk melakukannya, buat file rustfmt.toml dengan konten berikut:
indent_style = " Block "
max_width = 80
tab_spaces = 2
reorder_imports = false
reorder_modules = false
force_multiline_blocks = true
brace_style = " PreferSameLine "
control_brace_style = " AlwaysSameLine " Saya pribadi menggunakan vscode. Secara opsional, Anda dapat menambahkan konfigurasi ini di .vscode/settings.json Anda:
{
"editor.rulers" : [ 80 ],
"editor.tabSize" : 2 ,
"editor.detectIndentation" : false ,
"editor.trimAutoWhitespace" : true ,
"editor.formatOnSave" : true ,
"files.insertFinalNewline" : true ,
"files.trimTrailingWhitespace" : true ,
"rust-analyzer.showUnlinkedFileNotification" : false ,
"rust-analyzer.checkOnSave" : true ,
"rust-analyzer.check.command" : " clippy "
}Struktur direktori baru Anda akan terlihat seperti ini:
├── .gitignore
├── .vscode
│ └── settings.json
├── Cargo.lock
├── Cargo.toml
├── rustfmt.toml
└── src
└── main.rs Kami akan menggunakan NTEX sebagai kerangka kerja HTTP kami.
Kami dapat menginstal dependensi karat dengan menjalankan cargo add .
Perhatikan bahwa saat menggunakan ntex , kami memiliki kemampuan untuk memilih runtime kami.
Untuk meringkas dengan cepat, runtime akan mengelola pola async|await Anda.
Jika Anda terbiasa dengan nodejs runtime , itu agak mirip dalam penggunaan.
Untuk tutorial ini, kita akan menggunakan Tokio karena tampaknya menjadi pilihan yang lebih populer. Mari Tambahkan NTEX sebagai ketergantungan:
cargo add ntex --features tokio Kemudian kami akan memperbarui file main.rs kami dengan konten berikut:
use ntex :: web ;
# [ web :: get ( "/" ) ]
async fn index ( ) -> & ' static str {
"Hello world!"
}
# [ ntex :: main ]
async fn main ( ) -> std :: io :: Result < ( ) > {
web :: server ( || web :: App :: new ( ) . service ( index ) )
. bind ( ( "0.0.0.0" , 8080 ) ) ?
. run ( )
. await ? ;
Ok ( ( ) )
}Kami dapat menjalankan proyek kami dengan menggunakan perintah berikut:
cargo run Perintah ini akan menyusun kode kami dan menjalankannya.
Anda akan melihat output berikut:
Finished dev [unoptimized + debuginfo] target(s) in 17.38s
Running `target/debug/my-rest-api`Kami dapat menguji server kami menggunakan curl:
curl -v localhost:8080
* Trying 127.0.0.1:8080...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.68.0
> Accept: * / *
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-length: 12
< content-type: text/plain; charset=utf-8
< date: Fri, 26 May 2023 11:43:01 GMT
<
* Connection #0 to host localhost left intact
Hello world!% Selamat! Anda sekarang memiliki server HTTP pertama Anda di Rust !
Sekarang mari kita buat REST endpoints pertama kita.
Mengenai arsitektur direktori, sesuai dengan preferensi pribadi. Di ntex , kami menggunakan metode .service() untuk menambahkan endpoints baru. Karena itu, saya telah memilih untuk membuat direktori yang disebut services untuk menampung titik akhir saya.
Mari kita buat direktori:
mkdir src/services
touch src/services/mod.rs Perhatikan bahwa secara default, Rust mencoba mengimpor file mod.rs dari direktori kami.
Mari kita buat endpoints default kami di dalam services/mod.rs :
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
}Sekarang kita perlu menunjukkan bahwa kita ingin menggunakan modul ini di Main.rs kita:
use ntex :: web ;
mod services ;
# [ ntex :: main ]
async fn main ( ) -> std :: io :: Result < ( ) > {
web :: server ( || {
web :: App :: new ( )
// Default endpoint for unregisterd endpoints
. default_service ( web :: route ( ) . to ( services :: default )
)
} )
. bind ( ( "0.0.0.0" , 8080 ) ) ?
. run ( )
. await ? ;
Ok ( ( ) )
} Sekarang, untuk endpoints yang tidak terdaftar, kami akan memiliki kesalahan 404.
Sebelum melanjutkan, mari kita tambahkan empat dependensi: serde dan serde_json untuk serialisasi JSON, dan utoipa dengan utoipa-swagger-ui untuk memiliki kesombongan OpenAPI .
cargo add serde --features derive
cargo add serde_json utoipa utoipa-swagger-ui Selanjutnya, kita akan membuat tipe HttpError kita sendiri sebagai pembantu. Buat file di bawah src/error.rs dengan konten berikut:
use ntex :: web ;
use ntex :: http ;
use utoipa :: ToSchema ;
use serde :: { Serialize , Deserialize } ;
/// An http error response
# [ derive ( Clone , Debug , Serialize , Deserialize , ToSchema ) ]
pub struct HttpError {
/// The error message
pub msg : String ,
/// The http status code, skipped in serialization
# [ serde ( skip ) ]
pub status : http :: StatusCode ,
}
/// Helper function to display an HttpError
impl std :: fmt :: Display for HttpError {
fn fmt ( & self , f : & mut std :: fmt :: Formatter < ' _ > ) -> std :: fmt :: Result {
write ! ( f , "[{}] {}" , self . status , self . msg )
}
}
/// Implement standard error for HttpError
impl std :: error :: Error for HttpError { }
/// Helper function to convert an HttpError into a ntex::web::HttpResponse
impl web :: WebResponseError for HttpError {
fn error_response ( & self , _ : & web :: HttpRequest ) -> web :: HttpResponse {
web :: HttpResponse :: build ( self . status ) . json ( & self )
}
} Kami perlu mengimpor modul kesalahan kami di main.rs kami biarkan memperbarui:
use ntex :: web ;
mod error ;
mod services ;
# [ ntex :: main ]
async fn main ( ) -> std :: io :: Result < ( ) > {
web :: server ( || {
web :: App :: new ( )
// Default endpoint for unregisterd endpoints
. default_service ( web :: route ( ) . to ( services :: default )
)
} )
. bind ( ( "0.0.0.0" , 8080 ) ) ?
. run ( )
. await ? ;
Ok ( ( ) )
} Saya pikir kami siap untuk menulis beberapa contoh endpoints . Mari kita simulasikan daftar TODO dan buat file baru di bawah src/services/todo.rs :
use ntex :: web ;
# [ web :: get ( "/todos" ) ]
pub async fn get_todos ( ) -> web :: HttpResponse {
web :: HttpResponse :: Ok ( ) . finish ( )
}
# [ web :: post ( "/todos" ) ]
pub async fn create_todo ( ) -> web :: HttpResponse {
web :: HttpResponse :: Created ( ) . finish ( )
}
# [ web :: get ( "/todos/{id}" ) ]
pub async fn get_todo ( ) -> web :: HttpResponse {
web :: HttpResponse :: Ok ( ) . finish ( )
}
# [ web :: put ( "/todos/{id}" ) ]
pub async fn update_todo ( ) -> web :: HttpResponse {
web :: HttpResponse :: Ok ( ) . finish ( )
}
# [ web :: delete ( "/todos/{id}" ) ]
pub async fn delete_todo ( ) -> web :: HttpResponse {
web :: HttpResponse :: Ok ( ) . finish ( )
}
pub fn ntex_config ( cfg : & mut web :: ServiceConfig ) {
cfg . service ( get_todos ) ;
cfg . service ( create_todo ) ;
cfg . service ( get_todo ) ;
cfg . service ( update_todo ) ;
cfg . service ( delete_todo ) ;
} Kami perlu memperbarui src/services/mod.rs kami untuk mengimpor todo.rs kami:
pub mod todo ;
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
} Di main.rs kami:
use ntex :: web ;
mod error ;
mod services ;
# [ ntex :: main ]
async fn main ( ) -> std :: io :: Result < ( ) > {
web :: server ( || {
web :: App :: new ( )
// Register todo endpoints
. configure ( services :: todo :: ntex_config )
// Default endpoint for unregisterd endpoints
. default_service ( web :: route ( ) . to ( services :: default ) )
} )
. bind ( ( "0.0.0.0" , 8080 ) ) ?
. run ( )
. await ? ;
Ok ( ( ) )
} Mari kita buat beberapa struktur data untuk Todo kita. Kami akan membuat direktori baru src/models dengan mod.rs dan todo.rs -nya
mkdir src/models
touch src/models/mod.rs
touch src/models/todo.rs Di src/models/mod.rs kami, kami akan mengimpor todo.rs :
pub mod todo ; Dan di dalam src/models/todo.rs , kami akan menambahkan beberapa data structure :
use utoipa :: ToSchema ;
use serde :: { Serialize , Deserialize } ;
/// Todo model
# [ derive ( Clone , Debug , Serialize , Deserialize , ToSchema ) ]
pub struct Todo {
/// The todo id
pub id : i32 ,
/// The todo title
pub title : String ,
/// The todo completed status
pub completed : bool ,
}
/// Partial Todo model
# [ derive ( Clone , Debug , Serialize , Deserialize , ToSchema ) ]
pub struct TodoPartial {
/// The todo title
pub title : String ,
} Anda mungkin memperhatikan bahwa kami menggunakan makro serde dan utoipa untuk memungkinkan serialisasi dan konversi JSON ke OpenAPI Schema .
Jangan lupa memperbarui main.rs Anda untuk mengimpor models kami:
use ntex :: web ;
mod error ;
mod models ;
mod services ;
# [ ntex :: main ]
async fn main ( ) -> std :: io :: Result < ( ) > {
web :: server ( || {
web :: App :: new ( )
// Register todo endpoints
. configure ( services :: todo :: ntex_config )
// Default endpoint for unregisterd endpoints
. default_service ( web :: route ( ) . to ( services :: default ) )
} )
. bind ( ( "0.0.0.0" , 8080 ) ) ?
. run ( )
. await ? ;
Ok ( ( ) )
} Dengan model yang ada, kita sekarang dapat menghasilkan titik akhir jenis-aman dengan dokumentasi mereka. Mari kita perbarui endpoints kita di dalam src/services/todo.rs :
use ntex :: web ;
use crate :: models :: todo :: TodoPartial ;
/// List all todos
# [ utoipa :: path (
get ,
path = "/todos" ,
responses (
( status = 200 , description = "List of Todo" , body = [ Todo ] ) ,
) ,
) ]
# [ web :: get ( "/todos" ) ]
pub async fn get_todos ( ) -> web :: HttpResponse {
web :: HttpResponse :: Ok ( ) . finish ( )
}
/// Create a new todo
# [ utoipa :: path (
post ,
path = "/todos" ,
request_body = TodoPartial ,
responses (
( status = 201 , description = "Todo created" , body = Todo ) ,
) ,
) ]
# [ web :: post ( "/todos" ) ]
pub async fn create_todo (
_todo : web :: types :: Json < TodoPartial > ,
) -> web :: HttpResponse {
web :: HttpResponse :: Created ( ) . finish ( )
}
/// Get a todo by id
# [ utoipa :: path (
get ,
path = "/todos/{id}" ,
responses (
( status = 200 , description = "Todo found" , body = Todo ) ,
( status = 404 , description = "Todo not found" , body = HttpError ) ,
) ,
) ]
# [ web :: get ( "/todos/{id}" ) ]
pub async fn get_todo ( ) -> web :: HttpResponse {
web :: HttpResponse :: Ok ( ) . finish ( )
}
/// Update a todo by id
# [ utoipa :: path (
put ,
path = "/todos/{id}" ,
request_body = TodoPartial ,
responses (
( status = 200 , description = "Todo updated" , body = Todo ) ,
( status = 404 , description = "Todo not found" , body = HttpError ) ,
) ,
) ]
# [ web :: put ( "/todos/{id}" ) ]
pub async fn update_todo ( ) -> web :: HttpResponse {
web :: HttpResponse :: Ok ( ) . finish ( )
}
/// Delete a todo by id
# [ utoipa :: path (
delete ,
path = "/todos/{id}" ,
responses (
( status = 200 , description = "Todo deleted" , body = Todo ) ,
( status = 404 , description = "Todo not found" , body = HttpError ) ,
) ,
) ]
# [ web :: delete ( "/todos/{id}" ) ]
pub async fn delete_todo ( ) -> web :: HttpResponse {
web :: HttpResponse :: Ok ( ) . finish ( )
}
pub fn ntex_config ( cfg : & mut web :: ServiceConfig ) {
cfg . service ( get_todos ) ;
cfg . service ( create_todo ) ;
cfg . service ( get_todo ) ;
cfg . service ( update_todo ) ;
cfg . service ( delete_todo ) ;
}Dengan Utoipa, kami akan dapat melayani dokumentasi kesombongan kami.
Mari kita buat file baru di bawah src/services/openapi.rs :
use std :: sync :: Arc ;
use ntex :: web ;
use ntex :: http ;
use ntex :: util :: Bytes ;
use utoipa :: OpenApi ;
use crate :: error :: HttpError ;
use crate :: models :: todo :: { Todo , TodoPartial } ;
use super :: todo ;
/// Main structure to generate OpenAPI documentation
# [ derive ( OpenApi ) ]
# [ openapi (
paths (
todo :: get_todos ,
todo :: create_todo ,
todo :: get_todo ,
todo :: update_todo ,
todo :: delete_todo ,
) ,
components ( schemas ( Todo , TodoPartial , HttpError ) )
) ]
pub ( crate ) struct ApiDoc ;
# [ web :: get ( "/{tail}*" ) ]
async fn get_swagger (
tail : web :: types :: Path < String > ,
openapi_conf : web :: types :: State < Arc < utoipa_swagger_ui :: Config < ' static > > > ,
) -> Result < web :: HttpResponse , HttpError > {
if tail . as_ref ( ) == "swagger.json" {
let spec = ApiDoc :: openapi ( ) . to_json ( ) . map_err ( |err| HttpError {
status : http :: StatusCode :: INTERNAL_SERVER_ERROR ,
msg : format ! ( "Error generating OpenAPI spec: {}" , err ) ,
} ) ? ;
return Ok (
web :: HttpResponse :: Ok ( )
. content_type ( "application/json" )
. body ( spec ) ,
) ;
}
let conf = openapi_conf . as_ref ( ) . clone ( ) ;
match utoipa_swagger_ui :: serve ( & tail , conf . into ( ) ) . map_err ( |err| {
HttpError {
msg : format ! ( "Error serving Swagger UI: {}" , err ) ,
status : http :: StatusCode :: INTERNAL_SERVER_ERROR ,
}
} ) ? {
None => Err ( HttpError {
status : http :: StatusCode :: NOT_FOUND ,
msg : format ! ( "path not found: {}" , tail ) ,
} ) ,
Some ( file ) => Ok ( {
let bytes = Bytes :: from ( file . bytes . to_vec ( ) ) ;
web :: HttpResponse :: Ok ( )
. content_type ( file . content_type )
. body ( bytes )
} ) ,
}
}
pub fn ntex_config ( config : & mut web :: ServiceConfig ) {
let swagger_config = Arc :: new (
utoipa_swagger_ui :: Config :: new ( [ "/explorer/swagger.json" ] )
. use_base_layout ( ) ,
) ;
config . service (
web :: scope ( "/explorer/" )
. state ( swagger_config )
. service ( get_swagger ) ,
) ;
} Jangan lupa untuk memperbarui src/services/mod.rs untuk mengimpor src/services/openapi.rs :
pub mod todo ;
pub mod openapi ;
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
} Kemudian kami dapat memperbarui main.rs kami untuk mendaftarkan titik akhir penjelajah kami:
use ntex :: web ;
mod error ;
mod models ;
mod services ;
# [ ntex :: main ]
async fn main ( ) -> std :: io :: Result < ( ) > {
web :: server ( || {
web :: App :: new ( )
// Register swagger endpoints
. configure ( services :: openapi :: ntex_config )
// Register todo endpoints
. configure ( services :: todo :: ntex_config )
// Default endpoint for unregisterd endpoints
. default_service ( web :: route ( ) . to ( services :: default ) )
} )
. bind ( ( "0.0.0.0" , 8080 ) ) ?
. run ( )
. await ? ;
Ok ( ( ) )
}Kami baik untuk pergi. Mari Jalankan Server Kita:
cargo runMaka kita harus dapat mengakses penjelajah kita di http: // localhost: 8080/explorer/

Saya harap Anda akan mencoba menulis REST API Anda berikutnya di Rust!
Jangan lupa untuk melihat dokumentasi dependensi:
Buat gambar Docker Produksi! Tambahkan Dockerfile di direktori proyek Anda dengan konten berikut:
# Builder
FROM rust:1.69.0-alpine3.17 as builder
WORKDIR /app
# # Install build dependencies
RUN apk add alpine-sdk musl-dev build-base upx
# # Copy source code
COPY Cargo.toml Cargo.lock ./
COPY src ./src
# # Build release binary
RUN cargo build --release --target x86_64-unknown-linux-musl
# # Pack release binary with UPX (optional)
RUN upx --best --lzma /app/target/x86_64-unknown-linux-musl/release/my-rest-api
# Runtime
FROM scratch
# # Copy release binary from builder
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/my-rest-api /app
ENTRYPOINT [ "/app" ] Secara opsional Anda dapat menambahkan profil release ini di Cargo.toml Anda.
[ profile . release ]
opt-level = " z "
codegen-units = 1
strip = true
lto = trueIni akan mengoptimalkan biner rilis sekecil mungkin. Selain itu dengan UPX kita dapat membuat gambar Docker yang sangat kecil!
Bangun gambar Anda:
docker build -t my-rest-api:0.0.1 -f Dockerfile . 
Jika Anda ingin melihat dunia yang lebih nyata Usecase, saya mengundang Anda untuk melihat proyek OpenSource saya Nanocl. Itu mencoba menyederhanakan pengembangan dan penyebaran layanan mikro, dengan wadah atau mesin virtual!
Happy Coding!