Building a Todo Application with Actix
In this article, we'll build a full-stack Todo application using the Actix framework in Rust. We'll leverage the power of Actix for our backend while integrating a simple frontend to manage our Todo items. Let's dive right in!
Prerequisites
Before we start coding, ensure you have the following installed:
- Rust (Use
rustupfor easy installation) - Cargo (It comes bundled with Rust)
- Node.js (for managing our frontend dependencies)
Once you have these tools set up, create a new directory for our project:
mkdir todo_app
cd todo_app
Setting Up the Backend with Actix
Step 1: Create a New Actix Project
Let's begin by creating a new Actix project. Inside the todo_app directory, create a new folder called backend and navigate into that folder:
mkdir backend
cd backend
Now, create a new Rust project:
cargo new actix_todo
cd actix_todo
Step 2: Update Cargo.toml
Open Cargo.toml and add Actix dependencies. Your file should look something like this:
[package]
name = "actix_todo"
version = "0.1.0"
edition = "2018"
[dependencies]
actix-web = "4.0.0-beta.8"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
Step 3: Create the Todo Model
Next, we need a model for our Todo items. In src/main.rs, we'll define a struct and implement serialization:
#![allow(unused)] fn main() { use actix_web::{web, App, HttpServer, Responder, HttpResponse}; use serde::{Deserialize, Serialize}; use std::sync::Mutex; #[derive(Serialize, Deserialize, Clone)] struct Todo { id: usize, title: String, completed: bool, } struct AppState { todos: Mutex<Vec<Todo>>, } impl AppState { fn new() -> Self { AppState { todos: Mutex::new(Vec::new()), } } } }
Step 4: Define the API Endpoints
Now, let's create the endpoints for our Todo application. We’ll implement basic CRUD operations: Create, Read, Update, and Delete.
Add the following code in src/main.rs:
#![allow(unused)] fn main() { async fn get_todos(state: web::Data<AppState>) -> impl Responder { let todos = state.todos.lock().unwrap(); HttpResponse::Ok().json(&*todos) } async fn add_todo(todo: web::Json<Todo>, state: web::Data<AppState>) -> impl Responder { let mut todos = state.todos.lock().unwrap(); let new_id = todos.len() + 1; let mut new_todo = todo.into_inner(); new_todo.id = new_id; todos.push(new_todo); HttpResponse::Created().finish() } async fn update_todo(todo: web::Json<Todo>, state: web::Data<AppState>) -> impl Responder { let mut todos = state.todos.lock().unwrap(); if let Some(existing_todo) = todos.iter_mut().find(|t| t.id == todo.id) { existing_todo.title = todo.title.clone(); existing_todo.completed = todo.completed; HttpResponse::Ok().finish() } else { HttpResponse::NotFound().finish() } } async fn delete_todo(path: web::Path<usize>, state: web::Data<AppState>) -> impl Responder { let mut todos = state.todos.lock().unwrap(); if let Some(pos) = todos.iter().position(|t| t.id == *path) { todos.remove(pos); HttpResponse::NoContent().finish() } else { HttpResponse::NotFound().finish() } } }
Step 5: Set Up the Main Function
Now we'll put everything together in the main function, where we initialize the server:
#[actix_web::main] async fn main() -> std::io::Result<()> { let state = web::Data::new(AppState::new()); HttpServer::new(move || { App::new() .app_data(state.clone()) .route("/todos", web::get().to(get_todos)) .route("/todos", web::post().to(add_todo)) .route("/todos/{id}", web::put().to(update_todo)) .route("/todos/{id}", web::delete().to(delete_todo)) }) .bind("127.0.0.1:8080")? .run() .await }
Now you can run your backend server:
cargo run
Your backend API is now up at http://127.0.0.1:8080!
Setting Up the Frontend
Step 1: Create a Frontend Directory
Navigate back to your main todo_app directory and create a new folder for the frontend:
cd ..
mkdir frontend
cd frontend
Step 2: Initialize Node.js Project
Now, initialize a new Node.js project:
npm init -y
Step 3: Install Axios
We'll use Axios to handle HTTP requests to our backend API:
npm install axios
Step 4: Create the Frontend Structure
Create an index.html file in the frontend folder:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo App</title>
</head>
<body>
<h1>Todo Application</h1>
<input type="text" id="todoTitle" placeholder="Enter Todo Title" />
<button onclick="addTodo()">Add Todo</button>
<ul id="todoList"></ul>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="app.js"></script>
</body>
</html>
Step 5: Create app.js
Next, create app.js and implement functions to interact with our backend:
const todoList = document.getElementById('todoList');
async function fetchTodos() {
const response = await axios.get('http://127.0.0.1:8080/todos');
const todos = response.data;
todoList.innerHTML = '';
todos.forEach(todo => {
const li = document.createElement('li');
li.textContent = `${todo.title} - ${todo.completed ? "Completed" : "Not Completed"}`;
todoList.appendChild(li);
});
}
async function addTodo() {
const title = document.getElementById('todoTitle').value;
await axios.post('http://127.0.0.1:8080/todos', { title, completed: false });
document.getElementById('todoTitle').value = '';
fetchTodos();
}
fetchTodos();
Step 6: Open the Frontend
Finally, open the index.html file in your browser, and you should see a simple interface where you can add Todos and see the list.
Conclusion
Congratulations! You've built a simple Todo application using the Rust Actix framework for the backend and a basic JavaScript/HTML frontend for interaction. This application allows you to add, view, update, and delete Todo items.
Feel free to extend this project by adding features like editing Todo items, marking them as completed, or integrating user authentication for a more robust application. Actix provides a solid foundation for building high-performance applications, and with your new knowledge, you're well on your way to constructing even more complex systems.
Happy coding!