Volver

Rust for NodeJS developers (II) - Rocket

Diario del capitán, fecha estelar d162.y41/AB

Node.js Rust Web development Guides
Full-Stack Developer
Rust for NodeJS developers (II) - Rocket

This is the second article in our "Rust for NodeJS developers" series. If you missed the previous article, you can read it here: Rust for NodeJS developers (I) - Why and how?.

Rocket is a robust web framework for Rust, offering developers a streamlined approach to building high-performance web applications.
In this article, we'll dive into how to get started with Rocket.

Installing

Assuming Rust and Cargo are already set up, creating a new Rocket project is a breeze. Simply run the following command:

cargo new rust-for-nodejs-developers --bin

This command initializes a new Rust project named rust_for_nodejs_developers with a binary executable.
Next, navigate into your project directory:

cd rust-for-nodejs-developers

Now, let's add Rocket as a dependency in your Cargo.toml file.
Open the Cargo.toml file in your project directory and add the following line under the [dependencies] section:

rocket = { version = "0.5.1", features = ["json"] }

This line specifies that your project depends on the Rocket crate and specifies the version to use.
Be sure to check the Rocket website for the latest version.

With Rocket added as a dependency, you're all set to begin building your web application, let's create a src/main.rs file with the following code:

use rocket::{
    get, launch, routes,
    serde::json::{json, Value},
};

#[get("/")]
fn get_root() -> &'static str {
    "Hello, world!"
}

#[get("/users")]
fn get_users() -> Value {
    json!([{ "name": "user" }])
}

#[get("/users/<user_id>/friends")]
fn get_user_friends(user_id: u8) -> Value {
    json!([{ "friend_id": user_id }])
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![get_root, get_users, get_user_friends])
}

Now, simply execute cargo run and you'll see output similar to:

🔧 Configured for debug.
   >> address: 127.0.0.1
   >> port: 8000
   >> workers: 8
   >> max blocking threads: 512
   >> ident: Rocket
   >> IP header: X-Real-IP
   >> limits: bytes = 8KiB, data-form = 2MiB, file = 1MiB, form = 32KiB, json = 1MiB, msgpack = 1MiB, string = 8KiB
   >> temp dir: /tmp
   >> http/2: true
   >> keep-alive: 5s
   >> tls: disabled
   >> shutdown: ctrlc = true, force = true, signals = [SIGTERM], grace = 2s, mercy = 3s
   >> log level: normal
   >> cli colors: true
📬 Routes:
   >> (get_root) GET /
   >> (get_users) GET /users
   >> (get_user_friends) GET /users/<user_id>/friends
📡 Fairings:
   >> Shield (liftoff, response, singleton)
🛡️ Shield:
   >> X-Content-Type-Options: nosniff
   >> X-Frame-Options: SAMEORIGIN
   >> Permissions-Policy: interest-cohort=()
🚀 Rocket has launched from http://127.0.0.1:8000

Enhancing development experience using cargo-watch

An essential tool for streamlining the development process is cargo-watch, akin to nodemon, monitors for changes and automatically restart the server.

Install it with cargo install cargo-watch.

Then, instead of cargo run, utilize cargo watch -x run to start the server in "watch" mode.

Refining our Code

Now that we've configured our development environment, let's refine our code further.
Instead of cramming everything into a single file, we'll break it down into smaller and more modular components.
This approach facilitates maintenance and testing as our API grows in complexity.

We will start by refactoring from the deepest handler up to the root handler.

User friends handler

src/routes/users/user_friends/user_friends_routes.rs

use rocket::{
    get, routes,
    serde::json::{json, Value},
    Route,
};

pub fn user_friends_routes() -> Vec<Route> {
    routes![get_user_friends]
}

#[get("/<user_id>/friends")]
fn get_user_friends(user_id: u8) -> Value {
    json!([{ "friend_id": user_id }])
}

We have moved the handler to a new file while maintaining a folder structure that closely mirrors the related URL. Note that parameters cannot be separated from the handlers that require them.

src/routes/users/user_friends/mod.rs

mod user_friends_routes;

pub use user_friends_routes::user_friends_routes;

Here, we expose our public function and flatten the use route by re-exporting each method, making it directly accessible from this module.

This allows us to use it as use user_friends::user_friends_routes instead of use user_friends::user_friends_routes::user_friends_routes in other modules.

Users handler

src/routes/users/users_routes.rs

use rocket::{
    get, routes,
    serde::json::{json, Value},
    Route,
};

use super::user_friends::user_friends_routes;

pub fn users_routes() -> Vec<Route> {
    [routes![get_users], user_friends_routes()].concat()
}

#[get("/")]
fn get_users() -> Value {
    json!([{ "name": "user" }])
}

In this file, we export both the users and user_friends routes since both paths begin with /users.

src/routes/users/mod.rs

mod user_friends;
mod users_routes;

pub use users_routes::users_routes;

Here, we export and flatten our routes, and we also include the user_friends module.

Root Index Handler

src/routes/root_routes.rs

use rocket::{get, routes, Route};

pub fn root_routes() -> Vec<Route> {
    routes![get_root]
}

#[get("/")]
fn get_root() -> &'static str {
    "Hello, world!"
}

src/routes/mod.rs

    mod root_routes;
    mod users;

    pub use root_routes::root_routes;

This follows the same pattern as the other handlers.

Routes fairing

src/routes/routes_fairing.rs

use rocket::{
    fairing::{Fairing, Info, Kind, Result},
    Build, Rocket,
};

use super::{root_routes, users::users_routes};

pub struct RoutesFairing {}
#[rocket::async_trait]
impl Fairing for RoutesFairing {
    fn info(&self) -> Info {
        Info {
            name: "Routes module",
            kind: Kind::Ignite,
        }
    }

    async fn on_ignite(&self, rocket: Rocket<Build>) -> Result {
        Ok(rocket
            .mount("/", root_routes())
            .mount("/users", users_routes()))
    }
}

A fairing is Rocket's approach to structured middleware.

Here, we separate the logic required to mount each route. Each route node is responsible for mounting its child routes, cascading down the hierarchy.

src/routes/mod.rs

mod root_routes;
mod users;
mod routes_fairing;

pub use root_routes::root_routes;
pub use routes_fairing::RoutesFairing;

This adds the necessary lines to export and flatten the routes_fairing components.

Server Library

src/lib.rs

mod routes;

use rocket::{build, Build, Rocket};
use routes::RoutesFairing;

pub fn build_server() -> Rocket<Build> {
    build().attach(RoutesFairing {})
}

With this, we isolate the server build logic in a library, making it easier to test.

Main

src/main.rs

use rocket::launch;
use rust_for_nodejs_developers::build_server;

#[launch]
fn rocket() -> _ {
    build_server()
}

Finally, our main function is clean and ready to run, utilizing the build_server function we just defined.

Result

You can check the result in this branch.

Upcoming

In the next installment, we'll take our project to the next level by setting up a development environment using Docker. Stay tuned!

Compartir este post

Artículos relacionados

Rust for NodeJS developers (III) - Docker development environment

Rust for NodeJS developers (III) - Docker development environment

Docker can be beneficial not just for deploying applications but also for local development. By creating a Docker environment for our Rust API, we can ensure a consistent and isolated development experience across different machines and team members.

Leer el artículo
Rust for NodeJS developers (I) - Why and how?

Rust for NodeJS developers (I) - Why and how?

We are always open to exploring new technologies. Recently, Rust has caught our attention due to its high performance, memory safety and reliability. In this series of articles, we will share the experience of learning Rust as a Node.js developer by building a GraphQL API in Rust.

Leer el artículo
Coding

Use GoLang code in Ruby

GoLang has the option to create shared libraries in C, and in this post I will show you how to do it.

Leer el artículo