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, and serde
  • 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:

  1. Core concepts of RESTful endpoints and CRUD operations
  2. Building endpoints with Axum, sqlx, and serde
  3. Error handling and validation
  4. Hands-on practice and exercises
  5. 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

  1. Misconception: “All endpoints should return 200 OK.” Reality: Use appropriate status codes (201 for created, 404 for not found, etc.)
  2. 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:

  • NewTodo struct defines the expected JSON payload.
  • create_todo handler receives and processes the request.
  • The route /todos is 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:

  1. Define a handler for GET /todos
  2. Fetch all todos from the database
  3. 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

  1. Problem: Not validating input data Cause: Relying only on frontend validation Solution: Always validate on the backend

  2. 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

  1. How to implement CRUD endpoints in Rust with Axum
  2. Importance of validation and error handling
  3. Best practices for building robust APIs

Quick Review Questions

  1. What is the purpose of the serde crate?
  2. How do you handle errors in Axum handlers?
  3. 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