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 (
rustcandcargo). - [ ] VS Code with
rust-analyzer. - [ ] A tool to test APIs (like Postman, Insomnia, or just
curlin 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: Mutex<Vec<Task>>,
}
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())
}
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)
}
Key Concepts:
-
&rocket::State<AppState>: This is how Rocket injects our shared state into the function. -
Json<T>: Rocket automatically parses incoming JSON intoTand serializes outgoingTinto 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
- Dependencies: We added Rocket and Serde via
Cargo.toml. - Models: We created Rust structs that automatically map to JSON.
- State: We used a
Mutexto safely share data between requests. - Routes: We used
#[get]and#[post]macros to define endpoints. - 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:
- Integrate SQL database
- Learn how to handle asynchronous database queries.
- 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)