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

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • MyrinNew
    Senior Member
    • Feb 2024
    • 5168

    #1

    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







    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"







    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,
    }







    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: MutexVecTask>>,
    }







    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::StateAppState>) -> JsonVecTask>> {
    // Lock the mutex to read the data
    let tasks = state.tasks.lock().unwrap();
    // Return the list as JSON
    Json(tasks.clone())
    }







    2. Create a Task

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






    #[post("/tasks", format = "json", data = "")]
    fn create_task(
    state: &rocket::StateAppState>,
    new_task: JsonNewTask>
    ) -> JsonTask> {
    // 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)
    }







    Key Concepts:
    • &rocket::State: This is how Rocket injects our shared state into the function.
    • Json: 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
    }







    Step 6: Run and Test

    1. Run the Server

    In your terminal:






    cargo run







    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







    Response:






    [{"id":1,"title":"Learn Rust"},{"id":2,"title":"Build API"}]







    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"}"







    Response:






    {"id":3,"title":"Deploy to Production"}







    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!




    More...
Working...