How to use rocket crate in Rust web framework

Set up Rocket 0.5 by adding the crate, defining handlers with #[get]/#[post], and starting the server with #[launch]. Use State for shared data and Json for request bodies.

When you want a Rust web server that gets out of your way

You've decided to build a web app or a small HTTP API in Rust. You've heard of Actix and Axum and warp, and somewhere in the mix is a framework called Rocket that everyone says is friendly. You want endpoints defined with attributes, type-safe URL parameters, and not too many ceremony layers between you and a working "Hello, world." That's Rocket's pitch in one paragraph.

Rocket leans hard on macros and Rust's type system to make web code read more like a controller in Flask or FastAPI than like a wiring diagram. You'll write #[get("/users/<id>")] and Rocket will figure out how to parse the URL segment, give it to your handler as the right type, and return the response you produced. The cost is some compile-time pain (proc macros aren't fast) and a slightly opinionated structure. The benefit is that small apps stay genuinely small.

Setting up the project

Start with a fresh binary crate:

# Create a new binary project for your web service.
cargo new my_api
cd my_api

Then edit Cargo.toml. Modern Rocket (0.5 and later) is async by default, which means you don't need to add tokio separately. You just enable it through Rocket's feature.

# Cargo.toml
[package]
name = "my_api"
version = "0.1.0"
edition = "2021"

[dependencies]
# 0.5 is the modern, async, stable Rocket. The "json" feature pulls in
# serde-based JSON support, which you almost always want.
rocket = { version = "0.5", features = ["json"] }
serde = { version = "1", features = ["derive"] }

Older tutorials may show 0.4 and a synchronous API based on rocket::ignite(). Treat those as historical. The current entry point is rocket::build() and the launch is async.

The minimal "Hello, world"

Replace src/main.rs with this:

// `#[macro_use]` lets us use Rocket's macros (get, post, routes, launch)
// without prefixing them every time.
#[macro_use] extern crate rocket;

// A handler. The attribute defines the HTTP method and the URL pattern.
// Returning a string is the simplest possible response: it's sent
// as a 200 OK with content-type text/plain.
#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

// `#[launch]` builds and returns the Rocket instance. The runtime
// starts the server for us.
#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}

Run it:

cargo run

You'll see Rocket print its startup banner, the routes it mounted, and the address it's listening on (by default 127.0.0.1:8000). Hit it from another terminal:

curl http://127.0.0.1:8000/
# Hello, world!

A few things to call out about that tiny snippet. #[launch] is a Rocket macro that wraps the standard async runtime setup so you don't have to write #[rocket::main] and an explicit .launch().await (you can write that if you want; #[launch] is a shortcut). The _ return type is intentional: it means "let the compiler figure it out," because Rocket's builder type is unwieldy to write. And mount("/", routes![...]) is how you attach a list of routes to a base path; you can mount multiple groups under different prefixes if you like.

Path parameters and type-safe extraction

The thing Rocket is genuinely good at: turning bits of the URL into typed Rust values for you.

#[macro_use] extern crate rocket;

// `<id>` in the path is a placeholder. Rocket parses the segment
// and converts it to u32. If parsing fails, Rocket returns a 404
// without ever calling the handler.
#[get("/users/<id>")]
fn show_user(id: u32) -> String {
    format!("User #{id}")
}

// You can have multiple typed segments. Each one is parsed.
#[get("/posts/<year>/<slug>")]
fn show_post(year: u16, slug: &str) -> String {
    format!("Post from {year}: {slug}")
}

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

Behind the scenes, Rocket uses a trait called FromParam to convert the URL segment to your parameter type. Numbers, strings, and Uuid (with the right feature flag) all work out of the box. You can implement FromParam for your own types: a typical use is parsing IDs into a domain-specific newtype wrapper.

If somebody hits /users/abc, the u32 parse fails and the request short-circuits to a 404. You don't have to write any matching logic. Compare to a framework where you'd have to manually check the segment and return a 400. Rocket trades flexibility for ergonomic safety here.

Handling JSON requests and responses

Pretty much any real API takes JSON in and sends JSON out. Rocket's json feature makes that close to one line of code per side.

#[macro_use] extern crate rocket;

use rocket::serde::{Deserialize, Serialize, json::Json};

// Derive the serde traits so this struct can be (de)serialized.
// `crate = "rocket::serde"` is needed because Rocket re-exports serde
// and the derive macros need to know which path to use.
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct CreateUser {
    name: String,
    age: u8,
}

#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct UserResponse {
    id: u32,
    name: String,
}

// `data = "<input>"` tells Rocket to deserialize the request body
// as the type given (Json<CreateUser> here) and pass it as `input`.
#[post("/users", data = "<input>")]
fn create_user(input: Json<CreateUser>) -> Json<UserResponse> {
    let user = UserResponse {
        id: 42,
        name: input.name.clone(),
    };
    Json(user)
}

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

Test it from a terminal:

curl -X POST http://127.0.0.1:8000/users \
     -H 'Content-Type: application/json' \
     -d '{"name": "Ada", "age": 36}'
# {"id":42,"name":"Ada"}

If the JSON is malformed, Rocket returns a 400. If a required field is missing, Rocket returns a 422. You don't write any of that yourself. Same trade-off as before: less ceremony, less flexibility.

A more realistic example: a tiny notes service

Let's wire up a service with multiple routes, in-memory state, and proper error responses.

#[macro_use] extern crate rocket;

use rocket::{State, http::Status, response::status::Custom};
use rocket::serde::{Deserialize, Serialize, json::Json};
use std::sync::Mutex;

#[derive(Serialize, Clone)]
#[serde(crate = "rocket::serde")]
struct Note {
    id: u32,
    text: String,
}

#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct NewNote {
    text: String,
}

// Rocket's `State<T>` gives every handler access to shared state.
// We wrap a Vec in a Mutex so handlers can safely mutate it.
struct Db {
    notes: Mutex<Vec<Note>>,
}

#[get("/notes")]
fn list(db: &State<Db>) -> Json<Vec<Note>> {
    let guard = db.notes.lock().unwrap();
    Json(guard.clone())
}

#[get("/notes/<id>")]
fn show(id: u32, db: &State<Db>) -> Result<Json<Note>, Status> {
    let guard = db.notes.lock().unwrap();
    guard.iter().find(|n| n.id == id)
        .cloned()
        .map(Json)
        .ok_or(Status::NotFound)
}

#[post("/notes", data = "<input>")]
fn create(input: Json<NewNote>, db: &State<Db>) -> Custom<Json<Note>> {
    let mut guard = db.notes.lock().unwrap();
    let id = guard.len() as u32 + 1;
    let note = Note { id, text: input.text.clone() };
    guard.push(note.clone());
    Custom(Status::Created, Json(note))
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .manage(Db { notes: Mutex::new(Vec::new()) })
        .mount("/", routes![list, show, create])
}

The interesting bits are State<Db> (Rocket's dependency injection for shared state, registered with .manage(...)), the Mutex (because handlers run concurrently and a plain Vec isn't thread-safe), and Status::NotFound returned via Result (Rocket converts a Result::Err(Status) to that HTTP status automatically).

For real persistence you'd reach for SQLx, Diesel, or sea-orm and replace the in-memory Mutex<Vec<...>> with database calls. The shape of the code stays the same.

Common pitfalls

You're on Rocket 0.4 and copying examples from the docs. Symptoms include rocket::ignite() not existing, attributes prefixed with #[rocket::get] instead of plain #[get], and async errors. The fix: pin a major version explicitly in Cargo.toml and follow tutorials matching that version. The 0.4 to 0.5 migration was substantial.

You forgot #[macro_use] extern crate rocket; at the top of main.rs. Symptoms: routes! macro not found, #[get] not recognized. Fix: add the line. Yes, extern crate is mostly obsolete in Edition 2018+, but Rocket's macros are simpler to use this way.

You hit "the trait Sync is not implemented for X" when registering managed state. State must be Send + Sync because handlers can run on multiple threads. Wrap mutable bits in Mutex or RwLock.

You set up Rocket in production and it's only listening on localhost. Set ROCKET_ADDRESS=0.0.0.0 (or use a Rocket.toml) to bind on all interfaces. Same for ROCKET_PORT.

When to use Rocket vs alternatives

Rocket shines when you want a small, expressive web app and you don't mind a slightly slower compile. Tutorials and small services land here naturally.

Reach for Axum when you want tighter integration with the Tokio ecosystem, more explicit middleware composition, and a thinner abstraction layer. Axum is the current mainstream choice for production APIs.

Reach for Actix-web when you want maximum throughput in benchmarks and don't mind the actor-flavored API. It's older and has more battle-testing in industry.

The core skills transfer between them. Once you're comfortable with one, the others are an afternoon to pick up.

Where to go next

If you stick with Rocket, the framework's own guide is excellent. From here, you'll want to understand state and routing in more detail.

How to Build a Web Application with Rocket in Rust