Mini-Project - Core Endpoint Implementation
Learning Objectives: By the end of this lesson, you will be able to:
- Implement CRUD endpoints for your mini-project using Axum,
sqlx, andserde- Understand how to structure and test RESTful endpoints in Rust
- Apply best practices for error handling and validation in API development
1. Welcome and Introduction (5 minutes)
Icebreaker Activity
What’s the most satisfying feature you’ve ever built? Share with the group!
Lesson Overview
- Duration: 90 minutes
- Prerequisites: Basic Rust, familiarity with Axum, SQL basics
- Materials Needed: Rust toolchain, code editor, Postman/curl, database (e.g., SQLite/Postgres)
2. Learning Objectives and Session Outline (5 minutes)
What We’ll Cover Today:
- Core concepts of RESTful endpoints and CRUD operations
- Building endpoints with Axum,
sqlx, andserde - Error handling and validation
- Hands-on practice and exercises
- Common pitfalls and troubleshooting
Success Criteria:
By the end of this session, you should be able to design, implement, and test CRUD endpoints for your own mini-project.
3. Foundation Knowledge (15-20 minutes)
Core Concepts
What is a RESTful Endpoint?
A RESTful endpoint is a URL that represents a resource (like a todo item) and supports standard HTTP methods (GET, POST, PUT, DELETE) to interact with it.
Real-World Analogy: Think of a RESTful API as a restaurant menu: each endpoint is a menu item, and the HTTP methods are the ways you can interact with that item (order, update, remove, view).
Why This Matters: RESTful APIs are the backbone of modern web and mobile apps, enabling communication between frontend and backend.
CRUD Operations
- Create: Add a new resource (POST)
- Read: Retrieve resource(s) (GET)
- Update: Modify an existing resource (PUT/PATCH)
- Delete: Remove a resource (DELETE)
Axum, sqlx, and serde
- Axum: Web framework for building APIs in Rust
- sqlx: Async SQL toolkit for database access
- serde: Serialization/deserialization library for Rust
Key Terminology
- Endpoint: A specific URL path in your API (e.g.,
/todos) - Handler: Function that processes a request and returns a response
- Payload: Data sent with a request (usually in JSON)
4. Detailed Explanation and Examples (25-30 minutes)
Understanding Core Endpoint Implementation
To build a robust API, you need to:
- Define clear routes for each operation
- Validate and deserialize incoming data
- Interact with the database safely
- Handle errors gracefully
Practical Applications
- Powering web/mobile apps
- Automating business processes
- Integrating with third-party services
Common Misconceptions
- Misconception: “All endpoints should return 200 OK.” Reality: Use appropriate status codes (201 for created, 404 for not found, etc.)
- Misconception: “You don’t need to validate input if the frontend does.” Reality: Always validate on the backend for security and data integrity.
5. Live Coding Demonstration (20-25 minutes)
Example 1: Basic Implementation
// POST /todos - Create a new todo
use axum::{Json, Router, routing::post};
use serde::Deserialize;
#[derive(Deserialize)]
struct NewTodo {
title: String,
}
async fn create_todo(Json(payload): Json<NewTodo>) -> String {
format!("Created todo: {}", payload.title)
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/todos", post(create_todo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Explanation:
NewTodostruct defines the expected JSON payload.create_todohandler receives and processes the request.- The route
/todosis registered for POST requests.
Example 2: Practical Application (with Database)
// POST /todos with SQLite (using sqlx). This switches from Day 16's PostgreSQL example.
use axum::{extract::State, http::StatusCode, Json};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
#[derive(Deserialize)]
struct NewTodo {
title: String,
}
#[derive(Serialize, FromRow)]
struct Todo {
id: i64,
title: String,
}
async fn create_todo(
State(pool): State<SqlitePool>,
Json(payload): Json<NewTodo>,
) -> Result<(StatusCode, Json<Todo>), StatusCode> {
if payload.title.trim().is_empty() { return Err(StatusCode::BAD_REQUEST); }
let rec = sqlx::query_as::<_, Todo>("INSERT INTO todos (title) VALUES (?) RETURNING id, title")
.bind(payload.title)
.fetch_one(&pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok((StatusCode::CREATED, Json(rec)))
}
// After adding the list_todos handler below, register it with:
// Router::new().route("/todos", post(create_todo).get(list_todos)).with_state(pool)
For this SQLite variant, use sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "derive"] } and create a table with CREATE TABLE todos (id INTEGER PRIMARY KEY, title TEXT NOT NULL);. For an in-memory SQLite database, configure a single connection (or use a file-backed DB) so pooled connections share data.
Key Points:
- Demonstrates database interaction
- Uses error handling with
Result - Returns JSON response
Example 3: Advanced Technique (Validation & Error Handling)
// PUT /todos/{id} - Update a todo with validation
use axum::{extract::{Path, Json}, http::StatusCode};
use serde::Deserialize;
#[derive(Deserialize)]
struct UpdateTodo {
title: String,
}
async fn update_todo(
Path(id): Path<i64>,
Json(payload): Json<UpdateTodo>,
) -> Result<String, StatusCode> {
if payload.title.trim().is_empty() {
return Err(StatusCode::BAD_REQUEST);
}
// In the database-backed version, UPDATE ... RETURNING via sqlx;
// return 404 when the row does not exist.
Ok(format!("Updated todo {}: {}", id, payload.title))
}
Debugging Demonstration
Show common errors and how to fix them:
// Common mistake: Not handling empty title
async fn create_todo(Json(payload): Json<NewTodo>) -> String {
// This will accept empty titles!
format!("Created todo: {}", payload.title)
}
// Corrected version with validation
async fn create_todo(Json(payload): Json<NewTodo>) -> Result<String, axum::http::StatusCode> {
if payload.title.trim().is_empty() {
return Err(axum::http::StatusCode::BAD_REQUEST);
}
Ok(format!("Created todo: {}", payload.title))
}
6. Hands-On Practice (30-40 minutes)
Exercise 1: Basic Practice (10 minutes)
Objective: Implement a GET endpoint to list all todos.
Instructions:
- Define a handler for GET
/todos - Fetch all todos from the database
- Return as JSON
Starter Code:
// Handler signature
async fn list_todos(/* ... */) -> /* ... */ {
// ...fetch from DB...
}
Solution:
async fn list_todos(State(pool): State<SqlitePool>) -> Result<Json<Vec<Todo>>, axum::http::StatusCode> {
let todos = sqlx::query_as::<_, Todo>("SELECT id, title FROM todos ORDER BY id")
.fetch_all(&pool)
.await
.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(todos))
}
Exercise 2: Intermediate Challenge (15 minutes)
Objective: Implement DELETE /todos/{id} endpoint and register it with delete(delete_todo).
Requirements:
- Accepts a todo ID as a path parameter
- Deletes the todo from the database
- Returns appropriate status code
Hints:
- Use
Path<i64>to extract the ID - Check if the todo exists before deleting
Exercise 3: Creative Application (15 minutes)
Objective: Add validation to your POST and PUT endpoints.
Challenge: Ensure that the title is not empty and is less than 100 characters.
Extension Ideas:
- Add a completed field to todos
- Implement PATCH for partial updates
- Add pagination to GET endpoints
7. Common Pitfalls and Troubleshooting (10 minutes)
Frequent Mistakes
-
Problem: Not validating input data Cause: Relying only on frontend validation Solution: Always validate on the backend
-
Problem: Not handling database errors Cause: Assuming DB always succeeds Solution: Use proper error handling and return meaningful status codes
Debugging Tips
- Use logging to trace requests and errors
- Test endpoints with tools like Postman or curl
- Read error messages carefully and check stack traces
8. Best Practices and Professional Tips (5 minutes)
Industry Standards
- Use clear, consistent naming for endpoints
- Return appropriate HTTP status codes
- Separate business logic from handlers when possible
Next Steps in Your Learning Journey
- Explore authentication and authorization
- Learn about OpenAPI/Swagger for documentation
- Build a full-featured project with tests
9. Summary and Review (5 minutes)
Key Takeaways
- How to implement CRUD endpoints in Rust with Axum
- Importance of validation and error handling
- Best practices for building robust APIs
Quick Review Questions
- What is the purpose of the
serdecrate? - How do you handle errors in Axum handlers?
- Why should you always validate input on the backend?
For Instructors
Teaching Tips
- Encourage students to test each endpoint as they build
- Discuss real-world use cases for each CRUD operation
- Adjust timing based on student experience
Assessment Ideas
- Code review of student endpoints
- Short quiz on REST concepts and Rust syntax
- Group project: build a mini-API
Adaptation Notes
- For beginners: provide more starter code and walkthroughs
- For advanced: add authentication or async patterns
- Online: use live coding and screen sharing for demos