---
title: "API REST CRUD Completa com Axum"
url: "https://rustlang.com.br/projetos/api-rest-crud/"
markdown_url: "https://rustlang.com.br/projetos/api-rest-crud.MD"
description: "Construa uma API REST CRUD completa em Rust com Axum, armazenamento em memória com HashMap, serialização JSON e tratamento de erros."
date: "2026-02-24"
author: "Equipe Rust Brasil"
---

# API REST CRUD Completa com Axum

Construa uma API REST CRUD completa em Rust com Axum, armazenamento em memória com HashMap, serialização JSON e tratamento de erros.


Neste projeto vamos construir do zero uma **API REST CRUD completa** usando o framework [Axum](https://github.com/tokio-rs/axum). A API vai gerenciar uma coleção de tarefas (to-do items) com operações de criação, leitura, atualização e exclusão. Utilizaremos armazenamento em memória com `HashMap` protegido por `Arc<Mutex<>>`, serialização JSON com `serde`, e tratamento de erros robusto com códigos de status HTTP adequados.

Este é um projeto fundamental para quem quer desenvolver serviços web em Rust. Ao final, você terá uma API funcional que pode ser estendida com banco de dados, autenticação e muito mais.

## O Que Vamos Construir

Uma API REST para gerenciamento de tarefas com as seguintes funcionalidades:

- **GET /tarefas** — Listar todas as tarefas
- **GET /tarefas/:id** — Buscar uma tarefa específica
- **POST /tarefas** — Criar uma nova tarefa
- **PUT /tarefas/:id** — Atualizar uma tarefa existente
- **DELETE /tarefas/:id** — Remover uma tarefa
- Serialização e deserialização JSON automática
- Tratamento de erros com códigos HTTP apropriados (404, 400, 201, etc.)
- Estado compartilhado thread-safe com `Arc<Mutex<HashMap>>`

## Estrutura do Projeto

```
api-rest-crud/
├── Cargo.toml
└── src/
    └── main.rs
```

## Configurando o Projeto

Crie o projeto com o Cargo:

```bash
cargo new api-rest-crud
cd api-rest-crud
```

Edite o `Cargo.toml` com as dependências necessárias:

```toml
[package]
name = "api-rest-crud"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
uuid = { version = "1", features = ["v4"] }
```

Usamos `axum` como framework web, `tokio` como runtime assíncrono, `serde` para serialização JSON e `uuid` para gerar identificadores únicos.

## Passo 1: Definindo os Modelos de Dados

Primeiro, vamos definir as estruturas que representam nossas tarefas e o estado da aplicação:

```rust
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;

// Modelo de uma tarefa
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tarefa {
    pub id: String,
    pub titulo: String,
    pub descricao: String,
    pub concluida: bool,
}

// Dados para criar uma nova tarefa (sem id, pois será gerado)
#[derive(Debug, Deserialize)]
pub struct NovaTarefa {
    pub titulo: String,
    pub descricao: String,
}

// Dados para atualizar uma tarefa existente
#[derive(Debug, Deserialize)]
pub struct AtualizarTarefa {
    pub titulo: Option<String>,
    pub descricao: Option<String>,
    pub concluida: Option<bool>,
}

// Estado compartilhado da aplicação
pub type EstadoApp = Arc<Mutex<HashMap<String, Tarefa>>>;
```

A struct `Tarefa` representa o recurso completo com `id`, `titulo`, `descricao` e `concluida`. As structs `NovaTarefa` e `AtualizarTarefa` servem como DTOs (Data Transfer Objects) para receber dados do cliente. O tipo `EstadoApp` encapsula nosso "banco de dados" em memória de forma thread-safe usando `Arc` para compartilhamento entre threads e `Mutex` para acesso exclusivo.

## Passo 2: Implementando os Handlers

Cada handler corresponde a uma operação CRUD. Vamos implementá-los um por um:

```rust
use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::IntoResponse,
    Json,
};

// GET /tarefas — Listar todas as tarefas
async fn listar_tarefas(
    State(estado): State<EstadoApp>,
) -> impl IntoResponse {
    let tarefas = estado.lock().unwrap();
    let lista: Vec<Tarefa> = tarefas.values().cloned().collect();
    Json(lista)
}

// GET /tarefas/:id — Buscar uma tarefa por ID
async fn buscar_tarefa(
    State(estado): State<EstadoApp>,
    Path(id): Path<String>,
) -> Result<Json<Tarefa>, StatusCode> {
    let tarefas = estado.lock().unwrap();
    tarefas
        .get(&id)
        .cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

// POST /tarefas — Criar nova tarefa
async fn criar_tarefa(
    State(estado): State<EstadoApp>,
    Json(nova): Json<NovaTarefa>,
) -> impl IntoResponse {
    let tarefa = Tarefa {
        id: Uuid::new_v4().to_string(),
        titulo: nova.titulo,
        descricao: nova.descricao,
        concluida: false,
    };

    let mut tarefas = estado.lock().unwrap();
    tarefas.insert(tarefa.id.clone(), tarefa.clone());

    (StatusCode::CREATED, Json(tarefa))
}

// PUT /tarefas/:id — Atualizar tarefa existente
async fn atualizar_tarefa(
    State(estado): State<EstadoApp>,
    Path(id): Path<String>,
    Json(dados): Json<AtualizarTarefa>,
) -> Result<Json<Tarefa>, StatusCode> {
    let mut tarefas = estado.lock().unwrap();

    let tarefa = tarefas.get_mut(&id).ok_or(StatusCode::NOT_FOUND)?;

    if let Some(titulo) = dados.titulo {
        tarefa.titulo = titulo;
    }
    if let Some(descricao) = dados.descricao {
        tarefa.descricao = descricao;
    }
    if let Some(concluida) = dados.concluida {
        tarefa.concluida = concluida;
    }

    Ok(Json(tarefa.clone()))
}

// DELETE /tarefas/:id — Remover tarefa
async fn remover_tarefa(
    State(estado): State<EstadoApp>,
    Path(id): Path<String>,
) -> StatusCode {
    let mut tarefas = estado.lock().unwrap();

    if tarefas.remove(&id).is_some() {
        StatusCode::NO_CONTENT
    } else {
        StatusCode::NOT_FOUND
    }
}
```

Observe como cada handler usa `State(estado)` para acessar o estado compartilhado. O `Mutex` garante que apenas uma thread acesse o `HashMap` por vez. Usamos `Result<Json<T>, StatusCode>` para retornar o dado ou um código de erro HTTP, e o Axum converte isso automaticamente na resposta correta.

## Passo 3: Configurando as Rotas e Tratamento de Erros

Vamos criar um handler para responder a rotas não encontradas e uma função para montar o roteador:

```rust
use axum::Router;
use axum::routing::{get, post, put, delete};

// Handler para rotas não encontradas
async fn rota_nao_encontrada() -> impl IntoResponse {
    (
        StatusCode::NOT_FOUND,
        Json(serde_json::json!({
            "erro": "Rota não encontrada",
            "codigo": 404
        })),
    )
}

// Monta o roteador com todas as rotas
fn criar_roteador(estado: EstadoApp) -> Router {
    Router::new()
        .route("/tarefas", get(listar_tarefas).post(criar_tarefa))
        .route(
            "/tarefas/{id}",
            get(buscar_tarefa)
                .put(atualizar_tarefa)
                .delete(remover_tarefa),
        )
        .fallback(rota_nao_encontrada)
        .with_state(estado)
}
```

O Axum permite encadear múltiplos métodos HTTP na mesma rota usando `.get()`, `.post()`, `.put()` e `.delete()`. A função `fallback` captura qualquer requisição que não corresponda a nenhuma rota definida.

## Passo 4: Montando o main.rs Completo

Aqui está o código completo do `src/main.rs`:

```rust
use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::IntoResponse,
    routing::get,
    Json, Router,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;

// === Modelos ===

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tarefa {
    pub id: String,
    pub titulo: String,
    pub descricao: String,
    pub concluida: bool,
}

#[derive(Debug, Deserialize)]
pub struct NovaTarefa {
    pub titulo: String,
    pub descricao: String,
}

#[derive(Debug, Deserialize)]
pub struct AtualizarTarefa {
    pub titulo: Option<String>,
    pub descricao: Option<String>,
    pub concluida: Option<bool>,
}

pub type EstadoApp = Arc<Mutex<HashMap<String, Tarefa>>>;

// === Handlers ===

async fn listar_tarefas(State(estado): State<EstadoApp>) -> impl IntoResponse {
    let tarefas = estado.lock().unwrap();
    let lista: Vec<Tarefa> = tarefas.values().cloned().collect();
    Json(lista)
}

async fn buscar_tarefa(
    State(estado): State<EstadoApp>,
    Path(id): Path<String>,
) -> Result<Json<Tarefa>, StatusCode> {
    let tarefas = estado.lock().unwrap();
    tarefas
        .get(&id)
        .cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

async fn criar_tarefa(
    State(estado): State<EstadoApp>,
    Json(nova): Json<NovaTarefa>,
) -> impl IntoResponse {
    let tarefa = Tarefa {
        id: Uuid::new_v4().to_string(),
        titulo: nova.titulo,
        descricao: nova.descricao,
        concluida: false,
    };
    let mut tarefas = estado.lock().unwrap();
    tarefas.insert(tarefa.id.clone(), tarefa.clone());
    (StatusCode::CREATED, Json(tarefa))
}

async fn atualizar_tarefa(
    State(estado): State<EstadoApp>,
    Path(id): Path<String>,
    Json(dados): Json<AtualizarTarefa>,
) -> Result<Json<Tarefa>, StatusCode> {
    let mut tarefas = estado.lock().unwrap();
    let tarefa = tarefas.get_mut(&id).ok_or(StatusCode::NOT_FOUND)?;

    if let Some(titulo) = dados.titulo {
        tarefa.titulo = titulo;
    }
    if let Some(descricao) = dados.descricao {
        tarefa.descricao = descricao;
    }
    if let Some(concluida) = dados.concluida {
        tarefa.concluida = concluida;
    }

    Ok(Json(tarefa.clone()))
}

async fn remover_tarefa(
    State(estado): State<EstadoApp>,
    Path(id): Path<String>,
) -> StatusCode {
    let mut tarefas = estado.lock().unwrap();
    if tarefas.remove(&id).is_some() {
        StatusCode::NO_CONTENT
    } else {
        StatusCode::NOT_FOUND
    }
}

async fn rota_nao_encontrada() -> impl IntoResponse {
    (
        StatusCode::NOT_FOUND,
        Json(serde_json::json!({
            "erro": "Rota não encontrada",
            "codigo": 404
        })),
    )
}

// === Aplicação ===

#[tokio::main]
async fn main() {
    let estado: EstadoApp = Arc::new(Mutex::new(HashMap::new()));

    let app = Router::new()
        .route("/tarefas", get(listar_tarefas).post(criar_tarefa))
        .route(
            "/tarefas/{id}",
            get(buscar_tarefa)
                .put(atualizar_tarefa)
                .delete(remover_tarefa),
        )
        .fallback(rota_nao_encontrada)
        .with_state(estado);

    let endereco = "0.0.0.0:3000";
    println!("Servidor rodando em http://{}", endereco);

    let listener = tokio::net::TcpListener::bind(endereco).await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
```

O `main` cria o estado compartilhado, monta o roteador com todas as rotas e inicia o servidor na porta 3000. O `tokio::main` configura o runtime assíncrono automaticamente.

## Como Executar

Compile e inicie o servidor:

```bash
cargo run
```

Em outro terminal, teste as operações com `curl`:

```bash
# Criar uma tarefa
curl -X POST http://localhost:3000/tarefas \
  -H "Content-Type: application/json" \
  -d '{"titulo": "Estudar Rust", "descricao": "Ler o capítulo sobre ownership"}'

# Resposta:
# {"id":"a1b2c3d4-...","titulo":"Estudar Rust","descricao":"Ler o capítulo sobre ownership","concluida":false}

# Listar todas as tarefas
curl http://localhost:3000/tarefas

# Buscar uma tarefa específica (substitua pelo id retornado)
curl http://localhost:3000/tarefas/a1b2c3d4-...

# Atualizar uma tarefa
curl -X PUT http://localhost:3000/tarefas/a1b2c3d4-... \
  -H "Content-Type: application/json" \
  -d '{"concluida": true}'

# Remover uma tarefa
curl -X DELETE http://localhost:3000/tarefas/a1b2c3d4-...
```

## Desafios para Expandir

1. **Adicionar paginação** — Implemente query parameters `?pagina=1&limite=10` para paginar a listagem de tarefas, retornando metadados como total de itens e total de páginas.

2. **Persistência com SQLite** — Substitua o `HashMap` em memória por um banco SQLite usando a crate `sqlx`, adicionando migrations e queries parametrizadas.

3. **Validação de entrada** — Use a crate `validator` para validar campos obrigatórios, tamanhos mínimos e máximos, e retorne mensagens de erro detalhadas em JSON.

4. **Filtros e busca** — Adicione query parameters para filtrar tarefas por status (`?concluida=true`) e buscar por título (`?busca=rust`).

5. **Logging estruturado** — Integre `tracing` e `tracing-subscriber` para registrar cada requisição com método, rota, status code e tempo de resposta.

## Veja Também

- [HashMap na Biblioteca Padrão](/stdlib/hashmap/) — Como funciona o HashMap que usamos como armazenamento
- [Mutex para Exclusão Mútua](/stdlib/mutex/) — Entenda o Mutex usado para proteger o estado
- [Arc para Compartilhamento entre Threads](/stdlib/rc-arc/) — Como Arc permite compartilhar dados entre tasks assíncronas
- [Axum vs Actix: Qual Framework Escolher?](/artigos/axum-vs-actix/) — Comparativo entre os principais frameworks web de Rust
- [Tutorial: API REST com Axum](/tutoriais/api-rest-axum/) — Tutorial complementar sobre APIs REST
