
เฮ้วันนี้ฉันต้องการแบ่งปันความรู้เกี่ยวกับวิธีการเขียน REST API ใน Rust มันอาจจะง่ายกว่าที่คุณคิด! เราจะไม่แสดงการเชื่อมต่อฐานข้อมูลในบทความนี้ แต่เรามุ่งเน้นไปที่การสาธิตวิธีการสร้างข้อกำหนดของ OpenAPI และให้บริการ Swagger UI
คุณสามารถค้นหาแหล่งที่มาของรหัสเต็มบน GitHub
ก่อนเริ่มต้นตรวจสอบให้แน่ใจว่าคุณติดตั้งสนิมแล้ว
เริ่มต้นด้วยการเริ่มต้นโครงการใหม่โดยใช้ cargo init
cargo init my-rest-api
cd my-rest-apiที่ควรสร้างโครงสร้างไดเรกทอรีต่อไปนี้:
├── Cargo.toml
└── src
└── main.rs คุณสามารถใช้ rustfmt สำหรับการจัดรูปแบบ ในการทำเช่นนั้นให้สร้างไฟล์ rustfmt.toml ด้วยเนื้อหาต่อไปนี้:
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 " ฉันใช้ vscode เป็นการส่วนตัว คุณสามารถเพิ่มการกำหนดค่านี้ใน .vscode/settings.json :
{
"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 "
}โครงสร้างไดเรกทอรีใหม่ของคุณควรมีลักษณะเช่นนี้:
├── .gitignore
├── .vscode
│ └── settings.json
├── Cargo.lock
├── Cargo.toml
├── rustfmt.toml
└── src
└── main.rs เราจะใช้ NTEX เป็นกรอบ HTTP ของเรา
เราสามารถติดตั้งการพึ่งพาสนิมโดยใช้ cargo add
โปรดทราบว่าเมื่อใช้ ntex เรามีความสามารถในการเลือก runtime ของเรา
หากต้องการสรุปอย่างรวดเร็ว runtime จะจัดการ async|await รูปแบบ
หากคุณคุ้นเคยกับ nodejs runtime มันจะคล้ายกันในการใช้งาน
สำหรับบทช่วยสอนนี้เราจะใช้ Tokio เพราะดูเหมือนว่าจะเป็นตัวเลือกที่ได้รับความนิยมมากขึ้น มาเพิ่ม NTEX เป็นการพึ่งพา:
cargo add ntex --features tokio จากนั้นเราจะอัปเดตไฟล์ main.rs ของเราด้วยเนื้อหาต่อไปนี้:
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 ( ( ) )
}เราสามารถเรียกใช้โครงการของเราโดยใช้คำสั่งต่อไปนี้:
cargo run คำสั่งนี้จะรวบรวมรหัสของเราและเรียกใช้
คุณควรเห็นผลลัพธ์ต่อไปนี้:
Finished dev [unoptimized + debuginfo] target(s) in 17.38s
Running `target/debug/my-rest-api`เราสามารถทดสอบเซิร์ฟเวอร์ของเราโดยใช้ 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!% ยินดีด้วย! ตอนนี้คุณมีเซิร์ฟเวอร์ HTTP แรกของคุณใน Rust !
ตอนนี้เรามาสร้าง REST endpoints ครั้งแรกของเรา
เกี่ยวกับสถาปัตยกรรมไดเรกทอรีมันขึ้นอยู่กับความชอบส่วนตัว ใน ntex เราใช้วิธี .service() เพื่อเพิ่ม endpoints ใหม่ ดังนั้นฉันได้เลือกที่จะสร้างไดเรกทอรีที่เรียกว่า services to House จุดสิ้นสุดของฉัน
มาสร้างไดเรกทอรี:
mkdir src/services
touch src/services/mod.rs โปรดทราบว่าโดยค่าเริ่มต้น Rust พยายามนำเข้าไฟล์ mod.rs จากไดเรกทอรีของเรา
มาสร้าง endpoints ของเราภายใน services/mod.rs :
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
}ตอนนี้เราต้องระบุว่าเราต้องการใช้โมดูลนี้ใน main.rs ของเรา:
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 ( ( ) )
} ตอนนี้สำหรับ endpoints ที่ไม่ได้ลงทะเบียนเราจะมีข้อผิดพลาด 404
ก่อนที่จะดำเนินการต่อให้เพิ่มการพึ่งพาสี่ครั้ง: serde และ serde_json สำหรับการจัดลำดับ JSON และ utoipa กับ utoipa-swagger-ui เพื่อให้ OpenAPI Swagger
cargo add serde --features derive
cargo add serde_json utoipa utoipa-swagger-ui ต่อไปเราจะสร้างประเภท HttpError ของเราเองในฐานะผู้ช่วย สร้างไฟล์ภายใต้ src/error.rs ด้วยเนื้อหาต่อไปนี้:
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 )
}
} เราจำเป็นต้องนำเข้าโมดูลข้อผิดพลาดของเราใน main.rs ให้อัปเดต:
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 ( ( ) )
} ฉันคิดว่าเราพร้อมที่จะเขียน endpoints ตัวอย่าง มาจำลองรายการสิ่งที่ต้องทำและสร้างไฟล์ใหม่ภายใต้ 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 ) ;
} เราจำเป็นต้องอัปเดต src/services/mod.rs ของเราเพื่อนำเข้า todo.rs ของเรา:
pub mod todo ;
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
} ใน main.rs ของเรา:
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 ( ( ) )
} มาสร้างโครงสร้างข้อมูลบางอย่างสำหรับ Todo เราต้องทำ เราจะสร้างไดเรกทอรีใหม่ src/models ด้วย mod.rs และ todo.rs ของเขา
mkdir src/models
touch src/models/mod.rs
touch src/models/todo.rs ใน src/models/mod.rs ของเราเราจะนำเข้า todo.rs :
pub mod todo ; และภายใน src/models/todo.rs เราจะเพิ่ม 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 ,
} คุณอาจสังเกตเห็นว่าเราใช้มาโคร serde และ utoipa ที่ได้รับเพื่อเปิดใช้งานการทำให้เป็นอนุกรม JSON และการแปลงเป็น OpenAPI Schema
อย่าลืมอัปเดต main.rs ของคุณเพื่อนำเข้า models ของเรา:
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 ( ( ) )
} ด้วยโมเดลในสถานที่ตอนนี้เราสามารถสร้างจุดสิ้นสุดที่ปลอดภัยประเภทด้วยเอกสารของพวกเขา มาอัปเดต endpoints ของเราภายใน 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 ) ;
}ด้วย UTOIPA เราจะสามารถให้บริการเอกสารประกอบของเราได้
มาสร้างไฟล์ใหม่ภายใต้ 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 ) ,
) ;
} อย่าลืมอัปเดต src/services/mod.rs เพื่อนำเข้า src/services/openapi.rs :
pub mod todo ;
pub mod openapi ;
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
} จากนั้นเราสามารถอัปเดต main.rs ของเราเพื่อลงทะเบียนจุดสิ้นสุดของ Explorer:
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 ( ( ) )
}เราพร้อมที่จะไป มาเรียกใช้เซิร์ฟเวอร์ของเรากันเถอะ:
cargo runจากนั้นเราควรจะสามารถเข้าถึง Explorer ของเราได้ที่ http: // localhost: 8080/explorer/

ฉันหวังว่าคุณจะพยายามเขียน API REST ครั้งต่อไปของคุณใน Rust!
อย่าลืมดูเอกสารอ้างอิง:
สร้างภาพนักเทียบท่า! เพิ่ม Dockerfile ในไดเรกทอรีโครงการของคุณด้วยเนื้อหาต่อไปนี้:
# 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" ] ทางเลือกคุณสามารถเพิ่มโปรไฟล์ release นี้ใน Cargo.toml :
[ profile . release ]
opt-level = " z "
codegen-units = 1
strip = true
lto = trueสิ่งนี้จะเพิ่มประสิทธิภาพไบนารีรีลีสให้เล็กที่สุดเท่าที่จะทำได้ นอกจากนี้ด้วย UPX เราสามารถสร้างภาพนักเทียบท่าขนาดเล็กจริงๆ!
สร้างภาพของคุณ:
docker build -t my-rest-api:0.0.1 -f Dockerfile . 
หากคุณต้องการเห็นโลกแห่งความเป็นจริงมากขึ้นฉันขอเชิญคุณมาดู Nanocl โครงการ OpenSource ของฉัน ที่พยายามทำให้การพัฒนาและการปรับใช้บริการไมโครง่ายขึ้นด้วยคอนเทนเนอร์หรือเครื่องเสมือนจริง!
การเข้ารหัสมีความสุข!