---
title: "Encurtador de URLs em Rust"
url: "https://rustlang.com.br/projetos/url-shortener/"
markdown_url: "https://rustlang.com.br/projetos/url-shortener.MD"
description: "Construa um serviço encurtador de URLs em Rust com Axum, geração de códigos curtos, redirecionamento HTTP e contagem de cliques."
date: "2026-02-24"
author: "Equipe Rust Brasil"
---

# Encurtador de URLs em Rust

Construa um serviço encurtador de URLs em Rust com Axum, geração de códigos curtos, redirecionamento HTTP e contagem de cliques.


Neste projeto vamos construir um **serviço encurtador de URLs** completo em Rust usando Axum. O serviço vai receber URLs longas, gerar códigos curtos únicos, redirecionar visitantes e contar os cliques de cada link. É um projeto clássico que ensina conceitos fundamentais de desenvolvimento web: roteamento, redirecionamento HTTP, geração de identificadores e gerenciamento de estado.

Encurtadores de URLs são serviços usados diariamente por milhões de pessoas. Construir um do zero vai mostrar como Rust lida com requisições web de forma extremamente eficiente.

## O Que Vamos Construir

Um serviço encurtador de URLs com as seguintes funcionalidades:

- **POST /api/encurtar** — Recebe uma URL longa e retorna um código curto
- **GET /:codigo** — Redireciona para a URL original
- **GET /api/stats/:codigo** — Retorna estatísticas de cliques
- **GET /api/links** — Lista todos os links encurtados
- Geração de códigos curtos aleatórios de 6 caracteres
- Contagem automática de cliques em cada redirecionamento
- Registro de data/hora de criação

## Estrutura do Projeto

```
url-shortener/
├── Cargo.toml
└── src/
    └── main.rs
```

## Configurando o Projeto

```bash
cargo new url-shortener
cd url-shortener
```

Configure o `Cargo.toml`:

```toml
[package]
name = "url-shortener"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rand = "0.8"
chrono = { version = "0.4", features = ["serde"] }
```

Usamos `rand` para gerar códigos aleatórios e `chrono` para registrar datas de criação.

## Passo 1: Definindo os Modelos e Geração de Códigos

Vamos definir as estruturas de dados e a função que gera códigos curtos:

```rust
use chrono::{DateTime, Utc};
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

// Representa um link encurtado armazenado no sistema
#[derive(Debug, Clone, Serialize)]
pub struct LinkEncurtado {
    pub codigo: String,
    pub url_original: String,
    pub url_curta: String,
    pub cliques: u64,
    pub criado_em: DateTime<Utc>,
}

// Dados recebidos para encurtar uma URL
#[derive(Debug, Deserialize)]
pub struct RequisicaoEncurtar {
    pub url: String,
}

// Resposta ao encurtar uma URL
#[derive(Debug, Serialize)]
pub struct RespostaEncurtar {
    pub codigo: String,
    pub url_curta: String,
    pub url_original: String,
}

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

// Gera um código curto aleatório de 6 caracteres alfanuméricos
fn gerar_codigo() -> String {
    let caracteres: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    let mut rng = rand::thread_rng();
    (0..6)
        .map(|_| {
            let indice = rng.gen_range(0..caracteres.len());
            caracteres[indice] as char
        })
        .collect()
}
```

A função `gerar_codigo` cria uma string de 6 caracteres aleatórios usando letras e números. Com 62 caracteres possíveis em 6 posições, temos mais de 56 bilhões de combinações possíveis.

## Passo 2: Implementando os Handlers da API

Agora vamos criar os handlers para cada endpoint:

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

const URL_BASE: &str = "http://localhost:3000";

// POST /api/encurtar — Cria um novo link encurtado
async fn encurtar_url(
    State(estado): State<EstadoApp>,
    Json(requisicao): Json<RequisicaoEncurtar>,
) -> Result<(StatusCode, Json<RespostaEncurtar>), (StatusCode, Json<serde_json::Value>)> {
    // Validação simples da URL
    if !requisicao.url.starts_with("http://") && !requisicao.url.starts_with("https://") {
        return Err((
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({
                "erro": "URL inválida. Deve começar com http:// ou https://"
            })),
        ));
    }

    let mut links = estado.lock().unwrap();

    // Gera um código único (tenta novamente se houver colisão)
    let codigo = loop {
        let candidato = gerar_codigo();
        if !links.contains_key(&candidato) {
            break candidato;
        }
    };

    let url_curta = format!("{}/{}", URL_BASE, codigo);

    let link = LinkEncurtado {
        codigo: codigo.clone(),
        url_original: requisicao.url.clone(),
        url_curta: url_curta.clone(),
        cliques: 0,
        criado_em: Utc::now(),
    };

    let resposta = RespostaEncurtar {
        codigo: codigo.clone(),
        url_curta,
        url_original: requisicao.url,
    };

    links.insert(codigo, link);

    Ok((StatusCode::CREATED, Json(resposta)))
}

// GET /:codigo — Redireciona para a URL original
async fn redirecionar(
    State(estado): State<EstadoApp>,
    Path(codigo): Path<String>,
) -> Result<Redirect, StatusCode> {
    let mut links = estado.lock().unwrap();

    if let Some(link) = links.get_mut(&codigo) {
        link.cliques += 1;
        let url = link.url_original.clone();
        Ok(Redirect::temporary(&url))
    } else {
        Err(StatusCode::NOT_FOUND)
    }
}

// GET /api/stats/:codigo — Estatísticas de um link
async fn estatisticas(
    State(estado): State<EstadoApp>,
    Path(codigo): Path<String>,
) -> Result<Json<LinkEncurtado>, StatusCode> {
    let links = estado.lock().unwrap();
    links
        .get(&codigo)
        .cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

// GET /api/links — Lista todos os links
async fn listar_links(State(estado): State<EstadoApp>) -> impl IntoResponse {
    let links = estado.lock().unwrap();
    let lista: Vec<LinkEncurtado> = links.values().cloned().collect();
    Json(lista)
}
```

O handler `redirecionar` incrementa o contador de cliques antes de redirecionar. Usamos `Redirect::temporary` (código 307) para que o navegador siga o redirecionamento automaticamente. A validação em `encurtar_url` verifica se a URL começa com um protocolo válido.

## Passo 3: Configurando as Rotas

Precisamos ter cuidado com a ordem das rotas para que `/:codigo` não capture requisições da API:

```rust
use axum::routing::get;
use axum::Router;

fn criar_app(estado: EstadoApp) -> Router {
    Router::new()
        // Rotas da API (definidas primeiro para ter prioridade)
        .route("/api/encurtar", axum::routing::post(encurtar_url))
        .route("/api/stats/{codigo}", get(estatisticas))
        .route("/api/links", get(listar_links))
        // Rota de redirecionamento (captura qualquer código)
        .route("/{codigo}", get(redirecionar))
        .with_state(estado)
}
```

As rotas com prefixo `/api/` são definidas antes da rota genérica `/{codigo}` para que o Axum as priorize corretamente.

## 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, Redirect},
    routing::get,
    Json, Router,
};
use chrono::{DateTime, Utc};
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

// === Modelos ===

#[derive(Debug, Clone, Serialize)]
pub struct LinkEncurtado {
    pub codigo: String,
    pub url_original: String,
    pub url_curta: String,
    pub cliques: u64,
    pub criado_em: DateTime<Utc>,
}

#[derive(Debug, Deserialize)]
pub struct RequisicaoEncurtar {
    pub url: String,
}

#[derive(Debug, Serialize)]
pub struct RespostaEncurtar {
    pub codigo: String,
    pub url_curta: String,
    pub url_original: String,
}

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

const URL_BASE: &str = "http://localhost:3000";

// === Funções auxiliares ===

fn gerar_codigo() -> String {
    let caracteres: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    let mut rng = rand::thread_rng();
    (0..6)
        .map(|_| {
            let indice = rng.gen_range(0..caracteres.len());
            caracteres[indice] as char
        })
        .collect()
}

// === Handlers ===

async fn encurtar_url(
    State(estado): State<EstadoApp>,
    Json(requisicao): Json<RequisicaoEncurtar>,
) -> Result<(StatusCode, Json<RespostaEncurtar>), (StatusCode, Json<serde_json::Value>)> {
    if !requisicao.url.starts_with("http://") && !requisicao.url.starts_with("https://") {
        return Err((
            StatusCode::BAD_REQUEST,
            Json(serde_json::json!({
                "erro": "URL inválida. Deve começar com http:// ou https://"
            })),
        ));
    }

    let mut links = estado.lock().unwrap();

    let codigo = loop {
        let candidato = gerar_codigo();
        if !links.contains_key(&candidato) {
            break candidato;
        }
    };

    let url_curta = format!("{}/{}", URL_BASE, codigo);

    let link = LinkEncurtado {
        codigo: codigo.clone(),
        url_original: requisicao.url.clone(),
        url_curta: url_curta.clone(),
        cliques: 0,
        criado_em: Utc::now(),
    };

    let resposta = RespostaEncurtar {
        codigo: codigo.clone(),
        url_curta,
        url_original: requisicao.url,
    };

    links.insert(codigo, link);
    Ok((StatusCode::CREATED, Json(resposta)))
}

async fn redirecionar(
    State(estado): State<EstadoApp>,
    Path(codigo): Path<String>,
) -> Result<Redirect, StatusCode> {
    let mut links = estado.lock().unwrap();
    if let Some(link) = links.get_mut(&codigo) {
        link.cliques += 1;
        let url = link.url_original.clone();
        Ok(Redirect::temporary(&url))
    } else {
        Err(StatusCode::NOT_FOUND)
    }
}

async fn estatisticas(
    State(estado): State<EstadoApp>,
    Path(codigo): Path<String>,
) -> Result<Json<LinkEncurtado>, StatusCode> {
    let links = estado.lock().unwrap();
    links
        .get(&codigo)
        .cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

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

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

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

    let app = Router::new()
        .route("/api/encurtar", axum::routing::post(encurtar_url))
        .route("/api/stats/{codigo}", get(estatisticas))
        .route("/api/links", get(listar_links))
        .route("/{codigo}", get(redirecionar))
        .with_state(estado);

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

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

## Como Executar

Compile e inicie o servidor:

```bash
cargo run
```

Teste com `curl`:

```bash
# Encurtar uma URL
curl -X POST http://localhost:3000/api/encurtar \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.rust-lang.org/pt-BR/learn"}'

# Resposta:
# {"codigo":"aB3xY7","url_curta":"http://localhost:3000/aB3xY7","url_original":"https://www.rust-lang.org/pt-BR/learn"}

# Testar redirecionamento (use -L para seguir redirects ou -I para ver headers)
curl -I http://localhost:3000/aB3xY7
# HTTP/1.1 307 Temporary Redirect
# location: https://www.rust-lang.org/pt-BR/learn

# Ver estatísticas
curl http://localhost:3000/api/stats/aB3xY7

# Listar todos os links
curl http://localhost:3000/api/links
```

## Desafios para Expandir

1. **Códigos customizados** — Permita que o usuário escolha um código curto personalizado (ex: `meu-site`) em vez de usar apenas geração aleatória, com validação de caracteres permitidos.

2. **Expiração de links** — Adicione um campo `expira_em` opcional para que links expirem automaticamente após um período definido, retornando 410 Gone quando acessados após a expiração.

3. **QR Code** — Integre a crate `qrcode` para gerar um QR Code PNG para cada URL encurtada, acessível via `GET /api/qr/:codigo`.

4. **Proteção contra abuso** — Implemente rate limiting por IP e bloqueio de URLs maliciosas usando uma lista negra de domínios conhecidos por phishing.

5. **Dashboard HTML** — Crie uma página web simples servida pelo próprio servidor que exibe todos os links com suas estatísticas em uma tabela HTML.

## Veja Também

- [HashMap na Biblioteca Padrão](/stdlib/hashmap/) — Estrutura usada como armazenamento dos links
- [String em Rust](/stdlib/string/) — Manipulação de strings para construir URLs
- [Gerando Hashes em Rust](/receitas/gerar-hash/) — Alternativa para geração de códigos curtos
- [Rust para Desenvolvimento Web](/artigos/rust-para-web/) — Panorama geral do ecossistema web em Rust
- [Criando um Servidor HTTP](/receitas/criar-servidor-http/) — Fundamentos de servidores HTTP
