DEV Community

Cover image for Building a REST API in Rust with Rocket (Part 2)
Ayas Hussein
Ayas Hussein

Posted on

Building a REST API in Rust with Rocket (Part 2)

Welcome back! In Part 1, we installed Rust, explored the basics of ownership, and set up our development environment. Now, it's time to build something real.

Today, we are building a REST API.

While there are several web frameworks in Rust (like Axum or Actix), we are choosing Rocket for this tutorial. Why? Because Rocket prioritizes developer ergonomics. It uses macros to make routing and data handling feel magical, allowing you to focus on logic rather than boilerplate.

By the end of this post, you will have a working API that can create and retrieve tasks.

Prerequisites

Ensure you have completed Part 1:

  • [ ] Rust installed (rustc and cargo).
  • [ ] VS Code with rust-analyzer.
  • [ ] A tool to test APIs (like Postman, Insomnia, or just curl in your terminal).

Step 1: Project Setup

Let's create a new project specifically for our API.

cargo new task_api
cd task_api
Enter fullscreen mode Exit fullscreen mode

Adding Dependencies

Open Cargo.toml. We need to add Rocket for the web server and Serde for handling JSON data.

[dependencies]
rocket = "0.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Enter fullscreen mode Exit fullscreen mode

Note: We are using Rocket 0.5, which supports Stable Rust. You do not need to install the nightly toolchain.

Step 2: Defining Data Models

In a REST API, we usually send and receive JSON. In Rust, we represent JSON objects as Structs. We use the serde library to automatically convert (serialize/deserialize) between Rust structs and JSON.

Open src/main.rs and add the following:

#[macro_use] extern crate rocket;

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

// 1. Define what a Task looks like
#[derive(Debug, Serialize, Deserialize)]
pub struct Task {
    pub id: u32,
    pub title: String,
}

// 2. Define what data the client sends to create a task
// (We don't need an ID, the server will generate that)
#[derive(Debug, Deserialize)]
pub struct NewTask {
    pub title: String,
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts:

  • #[derive(Serialize)]: Allows Rust to turn this struct into JSON.
  • #[derive(Deserialize)]: Allows Rust to turn incoming JSON into this struct.
  • pub: Makes the fields accessible outside the module.

Step 3: Managing State (In-Memory)

A real API needs a database. However, setting up SQL adds a lot of complexity for a first tutorial. To keep things focused on Rocket, we will store tasks in memory using a Vec wrapped in a Mutex.

  • Vec: A growable list.
  • Mutex: Allows safe access to data across multiple threads (important for web servers).

Add this below your structs:

use std::sync::Mutex;

// Our application state
pub struct AppState {
    tasks: Mutex<Vec<Task>>,
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Creating Routes (Endpoints)

Rocket uses macros to define routes. This makes the code very readable.

1. Get All Tasks

This endpoint will handle GET /tasks.

#[get("/tasks")]
fn get_tasks(state: &rocket::State<AppState>) -> Json<Vec<Task>> {
    // Lock the mutex to read the data
    let tasks = state.tasks.lock().unwrap();
    // Return the list as JSON
    Json(tasks.clone())
}
Enter fullscreen mode Exit fullscreen mode

2. Create a Task

This endpoint will handle POST /tasks. It accepts JSON in the body.

#[post("/tasks", format = "json", data = "<new_task>")]
fn create_task(
    state: &rocket::State<AppState>, 
    new_task: Json<NewTask>
) -> Json<Task> {
    // Lock the mutex to write data
    let mut tasks = state.tasks.lock().unwrap();

    // Generate a new ID (simple increment for demo)
    let new_id = if tasks.is_empty() { 1 } else { tasks.last().unwrap().id + 1 };

    // Create the full Task
    let task = Task {
        id: new_id,
        title: new_task.title.clone(),
    };

    // Save it
    tasks.push(task.clone());

    // Return the created task
    Json(task)
}
Enter fullscreen mode Exit fullscreen mode

Key Concepts:

  • &rocket::State<AppState>: This is how Rocket injects our shared state into the function.
  • Json<T>: Rocket automatically parses incoming JSON into T and serializes outgoing T into JSON.
  • lock().unwrap(): We must lock the Mutex to safely modify the vector.

Step 5: Putting It All Together (Main)

Finally, we need to tell Rocket to launch, register our routes, and manage our state.

Update your main function:

#[launch]
fn rocket() -> _ {
    // Initialize state with some dummy data
    let state = AppState {
        tasks: Mutex::new(vec![
            Task { id: 1, title: String::from("Learn Rust") },
            Task { id: 2, title: String::from("Build API") },
        ]),
    };

    rocket::build()
        .manage(state) // Register the state
        .mount("/", routes![get_tasks, create_task]) // Register routes
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Run and Test

1. Run the Server

In your terminal:

cargo run
Enter fullscreen mode Exit fullscreen mode

You should see output indicating the server is running on http://127.0.0.1:8000.

2. Test GET Request

Open your browser or terminal:

curl http://localhost:8000/tasks
Enter fullscreen mode Exit fullscreen mode

Response:

[{"id":1,"title":"Learn Rust"},{"id":2,"title":"Build API"}]
Enter fullscreen mode Exit fullscreen mode

3. Test POST Request

Use curl to send JSON data:

curl -X POST http://localhost:8000/tasks \
  -H "Content-Type: application/json" \
  -d "{\"title\":\"Deploy to Production\"}"
Enter fullscreen mode Exit fullscreen mode

Response:

{"id":3,"title":"Deploy to Production"}
Enter fullscreen mode Exit fullscreen mode

If you run the GET request again, you will see the new task in the list!

Summary of What We Built

  1. Dependencies: We added Rocket and Serde via Cargo.toml.
  2. Models: We created Rust structs that automatically map to JSON.
  3. State: We used a Mutex to safely share data between requests.
  4. Routes: We used #[get] and #[post] macros to define endpoints.
  5. Launch: We mounted routes and launched the server.

Limitations & What's Next

This API is great for learning, but it has a major limitation: Data is stored in memory. If you restart the server, all tasks are lost.

In a production application, you need a database.

In Part 3, we will:

  1. Integrate SQL database
  2. Learn how to handle asynchronous database queries.
  3. Implement DELETE and UPDATE endpoints.

You now have the foundation to build web services in Rust. Try adding a DELETE endpoint on your own before the next tutorial!

Top comments (0)