Skip to main content
This guide will walk you through creating your first working API endpoint with authentication, from zero to a fully functional REST API.

What You’ll Build

By the end of this tutorial, you’ll have:
  • A running Ironclad server
  • User registration and login endpoints
  • An authenticated endpoint to fetch user profiles
  • Understanding of the DDD architecture
Make sure you’ve completed the Installation guide before starting this tutorial.

Start the Server

First, start your Ironclad development server:
cargo run
You should see:
╔════════════════════════════════════════════════════╗
    🚀 Rust Ironclad Framework (DDD Architecture)  ║
╚════════════════════════════════════════════════════╝
📍 Server: http://127.0.0.1:8080
 PostgreSQL connected
🌐 Listening on http://127.0.0.1:8080
Your server is now running at http://127.0.0.1:8080.

Register a New User

Let’s create your first user using the registration endpoint:
curl -X POST http://127.0.0.1:8080/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "username": "john_doe",
    "password": "SecurePassword123"
  }'
Ironclad automatically validates input. Emails must be valid, usernames must be 3-20 characters, and passwords must be strong.
You’ll receive a response with the user details and a JWT token:
{
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "[email protected]",
    "username": "john_doe",
    "role": "User",
    "is_active": true,
    "created_at": "2026-03-04T10:30:00Z",
    "updated_at": "2026-03-04T10:30:00Z"
  },
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
}
Save the token from the response - you’ll need it for authenticated requests.

Login with Your User

You can also log in with existing credentials:
curl -X POST http://127.0.0.1:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "SecurePassword123"
  }'
The response contains the same user object and a new JWT token.

Access Protected Endpoints

Now let’s use the token to access a protected endpoint. Replace YOUR_TOKEN with the token you received:
curl -X GET http://127.0.0.1:8080/api/user/profile \
  -H "Authorization: Bearer YOUR_TOKEN"
Response:
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "email": "[email protected]",
  "username": "john_doe",
  "role": "User",
  "is_active": true,
  "created_at": "2026-03-04T10:30:00Z",
  "updated_at": "2026-03-04T10:30:00Z"
}

Understanding the Request Flow

Here’s what happens when you make a request to /api/user/profile:
1

Request hits the route

The request reaches the route defined in src/routes/api.rs:34:
src/routes/api.rs
.route("/profile", web::get().to(UserController::get_profile))
2

Authentication extractor validates JWT

The AuthUser extractor in the controller automatically validates the JWT token and extracts user information:
src/infrastructure/http/controllers/user_controller.rs
pub async fn get_profile(
    service: web::Data<Arc<UserService>>,
    user: AuthUser, // Extracts and validates JWT
) -> ApiResult<HttpResponse> {
    let user_response = service.get_user_by_id(&user.id).await?;
    Ok(HttpResponse::Ok().json(user_response))
}
3

Service layer handles business logic

The service queries the repository for user data:
src/application/services/user_service.rs
pub async fn get_user_by_id(&self, id: &str) -> Result<UserResponse, ApiError> {
    let user = self.repository.find_by_id(id).await?;
    Ok(user.to_response())
}
4

Repository queries the database

The repository executes a type-safe SQL query:
src/infrastructure/persistence/postgres/user_repository.rs
async fn find_by_id(&self, id: &str) -> Result<User, RepositoryError> {
    let user = sqlx::query_as::<_, User>(
        "SELECT * FROM users WHERE id = $1"
    )
    .bind(id)
    .fetch_one(&self.pool)
    .await?;
    Ok(user)
}
5

Response returned to client

The user entity is converted to a response DTO and returned as JSON.

Exploring Other Endpoints

List All Users (Public)

This endpoint doesn’t require authentication:
curl http://127.0.0.1:8080/api/noauth/users

Get User by ID

curl http://127.0.0.1:8080/api/user/550e8400-e29b-41d4-a716-446655440000

Health Check

Verify your server is healthy:
curl http://127.0.0.1:8080/api/administration/health
Response:
{
  "status": "healthy",
  "timestamp": "2026-03-04T10:30:00Z"
}

Architecture Overview

Ironclad follows Domain-Driven Design (DDD) with a clean 5-layer architecture:
┌─────────────────────────────────────┐
│  Routes Layer                       │ ← HTTP Routing (src/routes/)
├─────────────────────────────────────┤
│  Infrastructure Layer               │ ← Controllers, HTTP handlers (src/infrastructure/)
├─────────────────────────────────────┤
│  Application Layer                  │ ← Services, DTOs (src/application/)
├─────────────────────────────────────┤
│  Domain Layer                       │ ← Entities, Business Logic (src/domain/)
├─────────────────────────────────────┤
│  Interfaces Layer                   │ ← Repository Traits (src/interfaces/)
└─────────────────────────────────────┘
This separation ensures your business logic remains independent of frameworks and databases, making it easy to test and maintain.

Working with Validation

Ironclad uses the ValidatedJson extractor for automatic input validation:
src/infrastructure/http/controllers/auth_controller.rs
pub async fn register(
    service: web::Data<Arc<AuthService>>,
    req: ValidatedJson<RegisterUserRequest>,
) -> ApiResult<HttpResponse> {
    let response = service.register(req.0).await?;
    Ok(HttpResponse::Created().json(response))
}
The RegisterUserRequest DTO defines validation rules:
src/application/dtos/auth_dto.rs
#[derive(Debug, Deserialize, Validate)]
pub struct RegisterUserRequest {
    #[validate(email(message = "Invalid email format"))]
    pub email: String,
    
    #[validate(length(min = 3, max = 20))]
    pub username: String,
    
    #[validate(length(min = 8))]
    pub password: String,
}
If validation fails, Ironclad automatically returns a 400 Bad Request with error details.

Next Steps

Congratulations! You’ve successfully:
  • Started an Ironclad server
  • Registered and authenticated users
  • Made requests to protected endpoints
  • Understood the request flow
Now you can:
  1. Learn the Project Structure - Understand how code is organized in the Project Structure guide
  2. Add Your Own Endpoints - Create custom controllers and routes
  3. Explore Domain Models - Check out src/domain/entities/ to see business logic
  4. Configure Security - Customize JWT settings and bcrypt cost in .env

CLI Commands

Ironclad includes a powerful CLI tool for common tasks:
# Check database connection
cargo run --bin ironclad -- db-check

# Show framework version
cargo run --bin ironclad -- version

# Enable maintenance mode
cargo run --bin ironclad -- down

# Disable maintenance mode
cargo run --bin ironclad -- up

Testing Your API

You can use tools like Postman, Insomnia, or HTTPie to test your API:
curl -X POST http://127.0.0.1:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"SecurePassword123"}'

Common Issues

Invalid Token Error

If you receive a 401 Unauthorized error:
  • Ensure you’re including the Authorization: Bearer <token> header
  • Check that the token hasn’t expired (default: 24 hours)
  • Verify the JWT_SECRET hasn’t changed since token generation

Validation Errors

If your request fails validation:
  • Check the error message for specific field requirements
  • Ensure email format is valid
  • Verify username is 3-20 characters
  • Confirm password is at least 8 characters

Database Connection Error

If the server fails to connect to PostgreSQL:
# Verify PostgreSQL is running
pg_isready

# Test connection with CLI
cargo run --bin ironclad -- db-check

Build docs developers (and LLMs) love