
Oye, hoy quería compartir mi conocimiento sobre cómo escribir una API REST en Rust. ¡Puede ser más fácil de lo que piensas! No mostraremos la conectividad de la base de datos en este artículo. En cambio, nos centramos en demostrar cómo generar especificaciones OpenAPI y servir a una Swagger UI .
Puede encontrar la fuente de código completa en GitHub.
Antes de comenzar, asegúrese de tener óxido instalado.
Comencemos por inicializar un nuevo proyecto usando cargo init .
cargo init my-rest-api
cd my-rest-apiQue debería producir la siguiente estructura del directorio:
├── Cargo.toml
└── src
└── main.rs Puede usar rustfmt para formatear. Para hacerlo, cree un archivo rustfmt.toml con el siguiente contenido:
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 " Yo personalmente uso VScode. Opcionalmente, puede agregar esta configuración en su .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 "
}Su nueva estructura de directorio debería verse así:
├── .gitignore
├── .vscode
│ └── settings.json
├── Cargo.lock
├── Cargo.toml
├── rustfmt.toml
└── src
└── main.rs Vamos a usar NTEX como nuestro marco HTTP.
Podemos instalar dependencias de óxido ejecutando cargo add .
Tenga en cuenta que al usar ntex , tenemos la capacidad de elegir nuestro runtime .
Para resumir rápidamente, el runtime administrará su async|await el patrón.
Si está familiarizado con el nodejs runtime , es algo similar en uso.
Para este tutorial, vamos a usar Tokio, ya que parece ser la opción más popular. Agregamos NTEX como dependencia:
cargo add ntex --features tokio Luego vamos a actualizar nuestro archivo main.rs con el siguiente contenido:
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 ( ( ) )
}Podemos ejecutar nuestro proyecto usando el siguiente comando:
cargo run Este comando compilará nuestro código y lo ejecutará.
Debería ver la siguiente salida:
Finished dev [unoptimized + debuginfo] target(s) in 17.38s
Running `target/debug/my-rest-api`Podemos probar nuestro servidor usando 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!% ¡Felicidades! ¡Ahora tiene su primer servidor HTTP en Rust !
Ahora creemos nuestros primeros REST endpoints .
Con respecto a la arquitectura del directorio, depende de la preferencia personal. En ntex , usamos el método .service() para agregar nuevos endpoints . Por lo tanto, he elegido crear un directorio llamado services para albergar mis puntos finales.
Creemos el directorio:
mkdir src/services
touch src/services/mod.rs Tenga en cuenta que, de forma predeterminada, Rust intenta importar un archivo mod.rs de nuestros directorios.
Creemos nuestros endpoints predeterminados dentro de services/mod.rs :
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
}Ahora debemos indicar que queremos usar este módulo en nuestro 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 ( ( ) )
} Ahora, para cualquier endpoints no registrado, tendremos un error 404.
Antes de continuar, agregemos cuatro dependencias: serde y serde_json para la serialización JSON, y utoipa con utoipa-swagger-ui para tener una arrogancia OpenAPI .
cargo add serde --features derive
cargo add serde_json utoipa utoipa-swagger-ui A continuación, vamos a crear nuestro propio tipo HttpError como ayudantes. Cree un archivo en src/error.rs con el siguiente contenido:
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 )
}
} Necesitamos importar nuestro módulo de error en nuestro main.rs , deje actualizarlo:
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 ( ( ) )
} Creo que estamos listos para escribir algunos endpoints de ejemplo. Simulemos una lista de TODO y creemos un nuevo archivo en 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 ) ;
} Necesitamos actualizar nuestro src/services/mod.rs para importar nuestro todo.rs :
pub mod todo ;
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
} En nuestro 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 ( ( ) )
} Creemos alguna estructura de datos para nuestro Todo . Vamos a crear un nuevo directorio src/models con su mod.rs y un todo.rs
mkdir src/models
touch src/models/mod.rs
touch src/models/todo.rs En nuestro src/models/mod.rs vamos a importar todo.rs :
pub mod todo ; Y dentro de src/models/todo.rs , vamos a agregar algo de 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 ,
} Puede notar que usamos las macros serde y utoipa derivamos para habilitar la serialización y la conversión JSON al OpenAPI Schema .
No olvide actualizar su main.rs para importar nuestros 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 ( ( ) )
} Con los modelos en su lugar, ahora podemos generar puntos finales a prueba de tipo con su documentación. Actualicemos nuestros endpoints dentro de 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 ) ;
}Con Utoipa, podremos servir nuestra documentación de arrogancia.
Creemos un nuevo archivo en 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 ) ,
) ;
} No olvide actualizar src/services/mod.rs para importar src/services/openapi.rs :
pub mod todo ;
pub mod openapi ;
use ntex :: web ;
pub async fn default ( ) -> web :: HttpResponse {
web :: HttpResponse :: NotFound ( ) . finish ( )
} Luego podemos actualizar nuestro main.rs para registrar nuestros puntos finales del explorador:
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 ( ( ) )
}Estamos listos para irnos. Ejecutemos nuestro servidor:
cargo runEntonces deberíamos poder acceder a nuestro Explorer en http: // localhost: 8080/explorer/

¡Espero que intentes escribir tu próxima API REST en Rust!
No olvide echar un vistazo a la documentación de dependencias:
¡Crea una imagen de Docker de producción! Agregue un Dockerfile en su directorio de proyecto con el siguiente contenido:
# 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" ] Opcionalmente, puede agregar este perfil release en su Cargo.toml :
[ profile . release ]
opt-level = " z "
codegen-units = 1
strip = true
lto = trueEsto optimizará el binario de lanzamiento para que sea lo más pequeño posible. ¡Además con UPX podemos crear una imagen de Docker realmente pequeña!
Construye tu imagen:
docker build -t my-rest-api:0.0.1 -f Dockerfile . 
Si quieres ver un lugar en el mundo más real, te invito a echar un vistazo a mi proyecto OpenSource Nanocl. ¡Que intentan simplificar el desarrollo y la implementación de micro servicios, con contenedores o máquinas virtuales!
¡Feliz codificación!