
Hey, heute wollte ich mein Wissen darüber teilen, wie man eine REST -API in Rost schreibt. Es kann einfacher sein als Sie denken! Wir werden in diesem Artikel keine Datenbankkonnektivität vorstellen. Stattdessen haben wir uns darauf konzentriert, zu demonstrieren, wie man OpenAPI -Spezifikationen generiert und eine Swagger UI dient.
Sie finden die vollständige Codequelle auf GitHub.
Stellen Sie vor dem Start sicher, dass Sie Rost installiert haben.
Beginnen wir zunächst ein neues Projekt mit cargo init initialisieren.
cargo init my-rest-api
cd my-rest-apiDas sollte die folgende Verzeichnisstruktur erzeugen:
├── Cargo.toml
└── src
└── main.rs Sie können rustfmt zum Formatieren verwenden. Erstellen Sie dazu eine rustfmt.toml -Datei mit dem folgenden Inhalt:
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 " Ich persönlich benutze VSCODE. Optional können Sie diese Konfiguration in Ihr .vscode/settings.json hinzufügen:
{
"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 "
}Ihre neue Verzeichnisstruktur sollte so aussehen:
├── .gitignore
├── .vscode
│ └── settings.json
├── Cargo.lock
├── Cargo.toml
├── rustfmt.toml
└── src
└── main.rs Wir werden NTEX als HTTP -Framework verwenden.
Wir können Rostabhängigkeiten installieren, indem wir cargo add .
Beachten Sie, dass wir bei der Verwendung von ntex die Möglichkeit haben, unsere runtime auszuwählen.
Um schnell zusammenzufassen, verwaltet die runtime Ihr async|await -Muster.
Wenn Sie mit der nodejs runtime vertraut sind, ist dies in der Verwendung ähnlich.
Für dieses Tutorial werden wir Tokio verwenden, da es die beliebtere Wahl zu sein scheint. Fügen wir Ntex als Abhängigkeit hinzu:
cargo add ntex --features tokio Dann werden wir unsere main.rs -Datei mit folgenden Inhalten aktualisieren:
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 ( ( ) )
}Wir können unser Projekt mit dem folgenden Befehl ausführen:
cargo run Dieser Befehl wird unseren Code kompilieren und ihn ausführen.
Sie sollten die folgende Ausgabe sehen:
Finished dev [unoptimized + debuginfo] target(s) in 17.38s
Running `target/debug/my-rest-api`Wir können unseren Server mit Curl testen:
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!% Glückwunsch! Sie haben jetzt Ihren ersten HTTP -Server in Rust !
Lassen Sie uns nun unsere ersten REST endpoints erstellen.
In Bezug auf die Verzeichnisarchitektur liegt es an der persönlichen Präferenz. In ntex verwenden wir die Methode .service() um neue endpoints hinzuzufügen. Daher habe ich mich entschieden, ein Verzeichnis namens services zu erstellen, um meine Endpunkte zu unterbringen.
Erstellen wir das Verzeichnis:
mkdir src/services
touch src/services/mod.rs Beachten Sie, dass Rust standardmäßig versucht, eine mod.rs -Datei aus unseren Verzeichnissen zu importieren.
Erstellen wir unsere endpoints in services/mod.rs :
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
}Jetzt müssen wir angeben, dass wir dieses Modul in unseren Haupt- und Rücken verwenden möchten:
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 ( ( ) )
} Für alle nicht registrierten endpoints haben wir nun einen 404 -Fehler.
Fügen wir vor dem Fortfahren vier Abhängigkeiten hinzu: serde und serde_json für JSON-Serialisierung und utoipa mit utoipa-swagger-ui um eine OpenAPI Prahlerei zu haben.
cargo add serde --features derive
cargo add serde_json utoipa utoipa-swagger-ui Als nächstes werden wir unseren eigenen HttpError -Typ als Helfer erstellen. Erstellen Sie eine Datei unter src/error.rs mit dem folgenden Inhalt:
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 )
}
} Wir müssen unser Fehlermodul in unserem main.rs importieren. Lassen Sie es aktualisieren:
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 ( ( ) )
} Ich denke, wir sind bereit, einige endpoints zu schreiben. Lassen Sie uns eine Todo -Liste simulieren und eine neue Datei unter src/services/todo.rs erstellen:
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 ) ;
} Wir müssen unsere src/services/mod.rs aktualisieren, um unsere todo.rs zu importieren:
pub mod todo ;
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
} In unserer 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 ( ( ) )
} Lassen Sie uns eine Datenstruktur für unser Todo erstellen. Wir werden ein neues Verzeichnis src/models mit seinem mod.rs und einem todo.rs erstellen
mkdir src/models
touch src/models/mod.rs
touch src/models/todo.rs In unseren src/models/mod.rs werden wir todo.rs importieren:
pub mod todo ; In src/models/todo.rs werden wir einige data structure hinzufügen:
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 ,
} Sie können feststellen, dass wir die Makros von serde und utoipa verwenden, um JSON -Serialisierung und -umwandlung in OpenAPI Schema zu ermöglichen.
Vergessen Sie nicht, Ihre main.rs zu aktualisieren, um unsere models zu importieren:
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 ( ( ) )
} Mit den vorhandenen Modellen können wir jetzt mit ihrer Dokumentation Typ-sichere Endpunkte erstellen. Lassen Sie uns unsere endpoints in src/services/todo.rs aktualisieren:
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 ) ;
}Mit Utoipa können wir unsere Prahlerin -Dokumentation bedienen.
Erstellen wir eine neue Datei unter 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 ) ,
) ;
} Vergessen Sie nicht, src/services/mod.rs zu aktualisieren, um src/services/openapi.rs zu importieren:
pub mod todo ;
pub mod openapi ;
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
} Anschließend können wir unsere main.rs aktualisieren, um unsere Explorer -Endpunkte zu registrieren:
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 ( ( ) )
}Wir sind gut zu gehen. Lassen Sie uns unseren Server ausführen:
cargo runDann sollten wir in der Lage sein, auf unseren Explorer auf http: // localhost: 8080/explorer/zugreifen zu können

Ich hoffe, Sie werden versuchen, Ihre nächste REST -API in Rost zu schreiben!
Vergessen Sie nicht, sich die Dokumentation der Abhängigkeiten anzusehen:
Erstellen Sie ein Produktions -Docker -Bild! Fügen Sie in Ihrem Projektverzeichnis eine Dockerfile mit dem folgenden Inhalt hinzu:
# 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" ] Optional können Sie dieses release -Profil in Ihrer Cargo.toml hinzufügen. Toml:
[ profile . release ]
opt-level = " z "
codegen-units = 1
strip = true
lto = trueDadurch optimiert die Freigabe Binärdatei so klein wie möglich. Zusätzlich können wir mit UPX ein wirklich kleines Docker -Bild erstellen!
Erstellen Sie Ihr Bild:
docker build -t my-rest-api:0.0.1 -f Dockerfile . 
Wenn Sie eine realere Usecase sehen möchten, lade ich Sie ein, sich mein OpenSource -Projekt Nanocl anzusehen. Sie versuchen, die Entwicklung und Bereitstellung von Micro -Diensten mit Containern oder virtuellen Maschinen zu vereinfachen!
Happy Coding!