
Hé, aujourd'hui, je voulais partager mes connaissances sur la façon d'écrire une API de repos dans Rust. Cela peut être plus facile que vous ne le pensez! Nous ne présenterons pas la connectivité de la base de données dans cet article. Au lieu de cela, nous nous sommes concentrés sur la démonstration de la façon de générer des spécifications OpenAPI et de servir une Swagger UI .
Vous pouvez trouver la source de code complète sur GitHub.
Avant de commencer, assurez-vous que vous avez installé de la rouille.
Commençons par initialiser un nouveau projet à l'aide cargo init .
cargo init my-rest-api
cd my-rest-apiCela devrait produire la structure du répertoire suivant:
├── Cargo.toml
└── src
└── main.rs Vous pouvez utiliser rustfmt pour la mise en forme. Pour ce faire, créez un fichier rustfmt.toml avec le contenu suivant:
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 " J'utilise personnellement VScode. En éventuellement, vous pouvez ajouter cette configuration dans votre .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 "
}Votre nouvelle structure de répertoire devrait ressembler à ceci:
├── .gitignore
├── .vscode
│ └── settings.json
├── Cargo.lock
├── Cargo.toml
├── rustfmt.toml
└── src
└── main.rs Nous allons utiliser NTEX comme notre framework HTTP.
Nous pouvons installer des dépendances de la rouille en exécutant cargo add .
Notez que lors de l'utilisation ntex , nous avons la possibilité de choisir notre runtime .
Pour résumer rapidement, l' runtime gérera votre modèle async|await .
Si vous connaissez l' nodejs runtime , c'est un peu similaire dans l'utilisation.
Pour ce tutoriel, nous allons utiliser Tokio car il semble être le choix le plus populaire. Ajoutons NTEX comme dépendance:
cargo add ntex --features tokio Ensuite, nous allons mettre à jour notre fichier main.rs avec le contenu suivant:
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 ( ( ) )
}Nous pouvons exécuter notre projet en utilisant la commande suivante:
cargo run Cette commande compilera notre code et l'exécutera.
Vous devriez voir la sortie suivante:
Finished dev [unoptimized + debuginfo] target(s) in 17.38s
Running `target/debug/my-rest-api`Nous pouvons tester notre serveur à l'aide de 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!% Félicitations! Vous avez maintenant votre premier serveur HTTP à Rust !
Créons maintenant nos premiers REST endpoints .
En ce qui concerne l'architecture d'annuaire, c'est la préférence personnelle. Dans ntex , nous utilisons la méthode .service() pour ajouter de nouveaux endpoints . Par conséquent, j'ai choisi de créer un répertoire appelé services pour abriter mes points de terminaison.
Créons le répertoire:
mkdir src/services
touch src/services/mod.rs Notez que par défaut, Rust essaie d'importer un fichier mod.rs à partir de nos répertoires.
Créons nos endpoints par défaut dans services/mod.rs :
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
}Nous devons maintenant indiquer que nous voulons utiliser ce module dans notre principal.R:
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 ( ( ) )
} Maintenant, pour tous les endpoints non enregistrés, nous aurons une erreur 404.
Avant de continuer, ajoutons quatre dépendances: serde et serde_json pour la sérialisation JSON, et utoipa avec utoipa-swagger-ui pour avoir un fanfaron OpenAPI .
cargo add serde --features derive
cargo add serde_json utoipa utoipa-swagger-ui Ensuite, nous allons créer notre propre type HttpError en tant qu'aides. Créez un fichier sous src/error.rs avec le contenu suivant:
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 )
}
} Nous devons importer notre module d'erreur dans notre main.rs Laissez-le le mettre à jour:
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 ( ( ) )
} Je pense que nous sommes prêts à écrire des exemples endpoints . Simulons une liste TODO et créons un nouveau fichier sous 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 ) ;
} Nous devons mettre à jour notre src/services/mod.rs pour importer notre todo.rs :
pub mod todo ;
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
} Dans nos 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 ( ( ) )
} Créons une structure de données pour notre Todo . Nous allons créer un nouveau répertoire src/models avec son mod.rs et un todo.rs
mkdir src/models
touch src/models/mod.rs
touch src/models/todo.rs Dans notre src/models/mod.rs nous allons importer todo.rs :
pub mod todo ; Et à l'intérieur src/models/todo.rs , nous allons ajouter une 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 ,
} Vous remarquerez peut-être que nous utilisons les macros de dérive serde et utoipa pour activer la sérialisation et la conversion JSON en OpenAPI Schema .
N'oubliez pas de mettre à jour vos main.rs pour importer nos 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 ( ( ) )
} Avec les modèles en place, nous pouvons désormais générer des points de terminaison de type type avec leur documentation. Mettons à jour nos endpoints dans 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 ) ;
}Avec UTOIPA, nous pourrons servir notre documentation Swagger.
Créons un nouveau fichier sous 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 ) ,
) ;
} N'oubliez pas de mettre à jour src/services/mod.rs pour importer src/services/openapi.rs :
pub mod todo ;
pub mod openapi ;
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
} Ensuite, nous pouvons mettre à jour nos main.rs pour enregistrer nos points de terminaison 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 ( ( ) )
}Nous sommes prêts à partir. Exécutons notre serveur:
cargo runEnsuite, nous devrions pouvoir accéder à notre explorateur sur http: // localhost: 8080 / explorateur /

J'espère que vous essairez d'écrire votre prochaine API REST à Rust!
N'oubliez pas de jeter un œil à la documentation des dépendances:
Créez une image Docker de production! Ajoutez un Dockerfile dans votre répertoire de projet avec le contenu suivant:
# 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" ] Éventuellement, vous pouvez ajouter ce profil release dans votre Cargo.toml :
[ profile . release ]
opt-level = " z "
codegen-units = 1
strip = true
lto = trueCela optimisera le binaire de libération pour être aussi petit que possible. De plus, avec UPX, nous pouvons créer une image Docker vraiment petite!
Construisez votre image:
docker build -t my-rest-api:0.0.1 -f Dockerfile . 
Si vous voulez voir un monde plus réel ucase, je vous invite à jeter un œil à mon projet OpenSource Nanocl. Qui essaient de simplifier le développement et le déploiement de micro-services, avec des conteneurs ou des machines virtuelles!
Codage heureux!