Building a Simple Web API with Rust and Rocket

Last updated: April 13, 2025

1. Introduction: Meet Rocket

Rocket is a popular web framework for Rust that focuses on ease of use, type safety, and developer productivity. It makes heavy use of Rust's powerful macro system to provide declarative routing and request handling, often resulting in concise and readable code.

This guide provides a beginner-friendly walkthrough of building a simple RESTful API using Rocket. We'll cover setting up a project, defining routes using attributes, handling path parameters, and working with JSON data.

As always, ensure you have Rust and Cargo installed (see Getting Started with Rust).

2. Project Setup

2.1 A Note on Nightly Rust (Historically)

Historically, Rocket required the nightly version of the Rust compiler due to its extensive use of unstable procedural macro features. While recent versions (like Rocket 0.5+) have made significant strides towards stable Rust compatibility, some advanced features might still benefit from or require nightly.

For this basic guide, we will aim for stable Rust compatibility. If you encounter features requiring nightly, you can switch your toolchain using rustup override set nightly within your project directory (use with understanding).

2.2 Adding Dependencies

Create a new Rust binary project:

cargo new rocket_api
cd rocket_api

Add Rocket and Tokio (as Rocket uses it internally) to your Cargo.toml:

[package]
name = "rocket_api"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = { version = "0.5.0", features = ["json"] } # Check crates.io for latest v0.5+
# Note: Rocket uses Tokio internally, but usually you don't need to add it explicitly unless using Tokio features directly.
# If needed: tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] } # Needed for JSON feature

We enable the "json" feature flag for Rocket to easily handle JSON request bodies and responses using Serde.

Alternatively, use cargo add:

cargo add rocket@0.5.0 --features json
cargo add serde --features derive # Rocket's json feature uses serde

3. Creating a Basic Server

Replace the contents of src/main.rs with a minimal Rocket application:

#[macro_use] extern crate rocket; // Required for Rocket macros

// Define a handler function decorated with a route attribute
#[get("/")] // Handles GET requests to the root path "/"
fn index() -> &'static str {
    "Hello, Rocket World!"
}

#[launch] // Macro to generate the main function and launch Rocket
fn rocket() -> _ { // The return type `_` lets the compiler infer `impl Launch`
    println!("🚀 Rocket launching at http://127.0.0.1:8000");
    // Build the Rocket instance and mount routes
    rocket::build().mount("/", routes![index])
}

Key Rocket concepts:

  • #[macro_use] extern crate rocket;: Imports Rocket's macros into scope.
  • #[get("/")]: A route attribute macro defining an HTTP GET endpoint for the path /. Rocket has corresponding macros for other methods (#[post], #[put], #[delete], etc.).
  • fn index() -> ...: The handler function associated with the route. Rocket uses the function signature to determine how to handle requests and responses.
  • #[launch]: A special macro that generates the async main function needed to initialize and start the Rocket server.
  • rocket::build(): Creates a new Rocket application instance.
  • .mount("/", routes![index]): Mounts one or more routes (defined in the routes! macro) at a base path (/ in this case).

Run the application using cargo run. Rocket will start the server (defaulting to port 8000). Access http://127.0.0.1:8000/ to see "Hello, Rocket World!".

4. Routing with Attributes

Adding more routes is straightforward using attribute macros:

#[macro_use] extern crate rocket;

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

// New handler and route
#[get("/ping")]
fn ping() -> &'static str {
    "pong"
}

#[launch]
fn rocket() -> _ {
     println!("🚀 Rocket launching at http://127.0.0.1:8000");
    // Add the new route to the routes! macro
    rocket::build().mount("/", routes![index, ping])
}

Restart the server (cargo run) and visit http://127.0.0.1:8000/ping.

5. Handling Path Parameters

Rocket extracts path parameters directly into function arguments if the types match.

#[macro_use] extern crate rocket;

// ... (index, ping handlers) ...

// Define parameter in the route path 
#[get("/greet/")]
fn greet(name: &str) -> String { // Parameter type matches segment (&str)
    format!("Hello, {}!", name)
}

#[launch]
fn rocket() -> _ {
    println!("🚀 Rocket launching at http://127.0.0.1:8000");
    rocket::build().mount("/", routes![index, ping, greet]) // Mount the greet route
}

Accessing http://127.0.0.1:8000/greet/RocketFan will return "Hello, RocketFan!". Rocket automatically parses the <name> segment into the name: &str argument.

6. Handling JSON (Request and Response)

Rocket uses Serde (enabled via the "json" feature) for handling JSON.

6.1 Serde Dependency

Ensure serde is in Cargo.toml with the derive feature, and Rocket has the json feature enabled (as done in the setup step).

6.2 Defining Structs

Define Rust structs and derive Serialize (for responses) and/or Deserialize (for requests).

use serde::{Serialize, Deserialize}; // Import traits
use rocket::serde::json::Json; // Import Rocket's JSON wrapper

#[derive(Serialize)] // For JSON responses
struct StatusMessage {
    status: String,
    healthy: bool,
}

#[derive(Deserialize)] // For JSON request bodies
struct CreateUserRequest {
    username: String,
    email: String,
}

6.3 Returning JSON

Wrap your response struct in rocket::serde::json::Json.

// Add this handler
#[get("/status")]
fn status() -> Json {
    Json(StatusMessage {
        status: "OK".to_string(),
        healthy: true,
    })
}

// --- Remember to mount the route in the main `rocket()` function ---
// rocket::build().mount("/", routes![index, ping, greet, status])

Accessing /status will now return the JSON representation of StatusMessage.

6.4 Accepting JSON (POST Example)

To accept JSON in a request body (e.g., for a POST request), use Json<T> as a function argument, where T is your struct deriving Deserialize.

// Add this handler
#[post("/users", format = "json", data = "<user_data>")] // Specify format and data param name
fn create_user(user_data: Json) -> Json {
    // Access deserialized data via user_data.into_inner() or directly
    println!("Received new user: {} ({})", user_data.username, user_data.email);

    // In a real app, you'd save the user to a database here

    Json(StatusMessage { // Respond with success status
        status: "Created".to_string(),
        healthy: true, // Or appropriate status
    })
}

// --- Remember to mount the route in the main `rocket()` function ---
// rocket::build().mount("/", routes![..., status, create_user])

You can test this endpoint using curl or an API client:

curl -X POST http://127.0.0.1:8000/users \
  -H "Content-Type: application/json" \
  -d '{"username": "testuser", "email": "test@example.com"}'

7. Running and Testing

  • Run: Use cargo run. Rocket provides detailed startup logging.
  • Test: Use curl or API clients to hit your defined endpoints (/, /ping, /greet/<name>, /status, /users [POST]).

8. Conclusion and Next Steps

Rocket offers an expressive and type-safe way to build web APIs in Rust, leveraging attributes and macros for concise route definitions and request handling. We've covered the essentials: setup, routing, path parameters, and JSON handling.

Further areas to explore in Rocket include:

  • Request Guards (for validation, authentication).
  • Managing State.
  • Handling Forms and Query Strings.
  • Working with Databases.
  • Configuration and Environment Variables.
  • Templating Engines (for server-rendered HTML).

Rocket's focus on developer experience makes it a compelling choice for those who appreciate its declarative style, especially if coming from frameworks in other languages with similar philosophies.

9. Additional Resources

Related Articles

External Resources