Creating Your First Project

Build your first complete project with Topaz from start to finish. A comprehensive guide from development environment setup to deployment.

Platform/Rust Integration
This walkthrough blends Topaz with the Rust/Actix ecosystem. Examples show interop glue so you can deploy Topaz logic inside existing Rust services.

Build your first complete project with Topaz! We'll create a Todo Management API while experiencing all of Topaz's core features. 🚀

🎯 Project Overview

What we're building
Personal Todo Management REST API

Technologies used
Topaz web server, JSON processing, file storage, error handling

What you'll learn
Project structure, routing, data modeling, testing

📁 Project Setup

1. Create New Project

# Create new Topaz project
topaz new TodoAPI --template=web-api
cd TodoAPI

# Check project structure
tree .

Generated structure:

 src/
   ├─ main.tpz           # Main entry point
   ├─ models/            # Data models
   ├─ handlers/          # Request handlers
   └─ utils/             # Utility functions
 tests/                 # Test files
 data/                  # Data storage
 topaz.toml            # Project configuration
 README.md

2. Add Dependencies

# topaz.toml
[package]
name = "TodoAPI"
version = "0.1.0"
description = "REST API for personal todo management"

[dependencies]
web = "2.0"           # Web server framework
json = "1.5"          # JSON processing
uuid = "1.0"          # Unique ID generation
chrono = "0.9"        # Date/time handling
serde = "2.1"         # Serialization/deserialization

[dev-dependencies]
test-framework = "1.0"
http-client = "0.8"

🏗️ Data Modeling

Todo Model Definition

// src/models/todo.tpz
use chrono::{DateTime, Utc}
use uuid::Uuid
use serde::{Serialize, Deserialize}

#[derive(Serialize, Deserialize, Clone, Debug)]
struct Todo {
    id: Uuid,
    title: string,
    description: Option<string>,
    completed: bool,
    priority: Priority,
    createdAt: DateTime<Utc>,
    completedAt: Option<DateTime<Utc>>,
    tags: Array<string>
}

#[derive(Serialize, Deserialize, Clone, Debug)]
enum Priority {
    Low,
    Normal,
    High,
    Urgent
}

impl Todo {
    // Create new todo
    function new(
        title: string,
        description: Option<string> = None,
        priority: Priority = Priority::Normal
    ) -> Todo {
        return Todo {
            id: Uuid::new_v4(),
            title,
            description,
            completed: false,
            priority,
            createdAt: Utc::now(),
            completedAt: None,
            tags: []
        }
    }

    // Mark todo as completed
    function complete(&mut self) {
        self.completed = true
        self.completedAt = Some(Utc::now())
    }

    // Add tag
    function addTag(&mut self, newTag: string) {
        if !self.tags.contains(&newTag) {
            self.tags.push(newTag)
        }
    }

    // Get priority score for sorting
    function priorityScore(&self) -> int {
        match self.priority {
            case Priority::Urgent => 4
            case Priority::High => 3
            case Priority::Normal => 2
            case Priority::Low => 1
        }
    }
} test {
    // Todo creation test
    let todo = Todo::new("Learn Topaz".to_string(), None, Priority::High)
    assert todo.title == "Learn Topaz"
    assert todo.completed == false
    assert todo.priority == Priority::High

    // Completion test
    let mut todo = Todo::new("Test".to_string(), None, Priority::Normal)
    todo.complete()
    assert todo.completed == true
    assert todo.completedAt.is_some() == true

    // Tag addition test
    let mut todo = Todo::new("Project".to_string(), None, Priority::Normal)
    todo.addTag("development".to_string())
    todo.addTag("learning".to_string())
    todo.addTag("development".to_string())  // Duplicate attempt
    assert todo.tags.len() == 2
    assert todo.tags.contains(&"development".to_string())
    assert todo.tags.contains(&"learning".to_string())
}

Request/Response Models

// src/models/dto.tpz
use serde::{Serialize, Deserialize}
use uuid::Uuid

#[derive(Deserialize)]
struct CreateTodoRequest {
    title: string,
    description: Option<string>,
    priority: Option<Priority>,
    tags: Option<Array<string>>
}

#[derive(Deserialize)]
struct UpdateTodoRequest {
    title: Option<string>,
    description: Option<string>,
    priority: Option<Priority>,
    tags: Option<Array<string>>
}

#[derive(Serialize)]
struct TodoResponse {
    id: Uuid,
    title: string,
    description: Option<string>,
    completed: bool,
    priority: Priority,
    createdAt: string,
    completedAt: Option<string>,
    tags: Array<string>
}

impl From<Todo> for TodoResponse {
    function from(todo: Todo) -> TodoResponse {
        return TodoResponse {
            id: todo.id,
            title: todo.title,
            description: todo.description,
            completed: todo.completed,
            priority: todo.priority,
            createdAt: todo.createdAt.format("%Y-%m-%d %H:%M:%S").to_string(),
            completedAt: todo.completedAt.map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string()),
            tags: todo.tags
        }
    }
}

#[derive(Serialize)]
struct ApiResponse<T> {
    success: bool,
    data: Option<T>,
    message: string
}

impl<T> ApiResponse<T> {
    function success(data: T) -> ApiResponse<T> {
        return ApiResponse {
            success: true,
            data: Some(data),
            message: "Success".to_string()
        }
    }

    function error(message: string) -> ApiResponse<T> {
        return ApiResponse {
            success: false,
            data: None,
            message
        }
    }
}

💾 Data Storage

Simple File-based Storage

// src/storage/file_store.tpz
use std::fs
use std::path::Path
use uuid::Uuid
use crate::models::Todo

struct FileStore {
    dataPath: string
}

impl FileStore {
    function new(dataPath: string) -> Result<FileStore, string> {
        // Create data directory
        if !Path::new(&dataPath).exists() {
            fs::create_dir_all(&dataPath)
                .map_err(|e| "Failed to create data directory: {e}".to_string())?
        }

        return Ok(FileStore { dataPath })
    }

    function filePath(&self) -> string {
        return format!("{}/todos.json", self.dataPath)
    }

    // Load all todos
    function getAll(&self) -> Result<Array<Todo>, string> {
        let path = self.filePath()
        
        if !Path::new(&path).exists() {
            return Ok([])
        }

        let content = fs::read_to_string(&path)
            .map_err(|e| "Failed to read file: {e}".to_string())?

        let todos: Array<Todo> = serde_json::from_str(&content)
            .map_err(|e| "Failed to parse JSON: {e}".to_string())?

        return Ok(todos)
    }

    // Save all todos
    function saveAll(&self, todos: &Array<Todo>) -> Result<(), string> {
        let jsonString = serde_json::to_string_pretty(todos)
            .map_err(|e| "Failed to serialize JSON: {e}".to_string())?

        fs::write(self.filePath(), jsonString)
            .map_err(|e| "Failed to write file: {e}".to_string())?

        return Ok(())
    }

    // Add todo
    function add(&self, todo: Todo) -> Result<Todo, string> {
        let mut todos = self.getAll()?
        todos.push(todo.clone())
        self.saveAll(&todos)?
        return Ok(todo)
    }

    // Find todo by ID
    function findById(&self, id: Uuid) -> Result<Option<Todo>, string> {
        let todos = self.getAll()?
        let foundTodo = todos.iter().find(|todo| todo.id == id).cloned()
        return Ok(foundTodo)
    }

    // Update todo
    function update(&self, id: Uuid, updatedTodo: Todo) -> Result<Option<Todo>, string> {
        let mut todos = self.getAll()?
        
        return match todos.iter().position(|todo| todo.id == id) {
            case Some(index) => {
                todos[index] = updatedTodo.clone()
                self.saveAll(&todos)?
                Ok(Some(updatedTodo))
            }
            case None => Ok(None)
        }
    }

    // Delete todo
    function delete(&self, id: Uuid) -> Result<bool, string> {
        let mut todos = self.getAll()?
        
        return match todos.iter().position(|todo| todo.id == id) {
            case Some(index) => {
                todos.remove(index)
                self.saveAll(&todos)?
                Ok(true)
            }
            case None => Ok(false)
        }
    }

    // Search todos with filter
    function search<F>(&self, filter: F) -> Result<Array<Todo>, string>
    where
        F: Fn(&Todo) -> bool
    {
        let todos = self.getAll()?
        let result = todos.into_iter().filter(filter).collect()
        return Ok(result)
    }
} test {
    use tempfile::tempdir

    // Test in temporary directory
    let tempDir = tempdir().unwrap()
    let store = FileStore::new(tempDir.path().to_str().unwrap().to_string()).unwrap()

    // Add todo test
    let newTodo = Todo::new("Test Todo".to_string(), None, Priority::Normal)
    let addedTodo = store.add(newTodo.clone()).unwrap()
    assert addedTodo.id == newTodo.id

    // Find todo test
    let foundTodo = store.findById(newTodo.id).unwrap()
    assert foundTodo.is_some()
    assert foundTodo.unwrap().title == "Test Todo"

    // Get all todos test
    let allTodos = store.getAll().unwrap()
    assert allTodos.len() == 1
}

🌐 Web Server and Routing

Main Server Setup

// src/main.tpz
mod models
mod storage
mod handlers

use web::{App, HttpServer, middleware}
use storage::FileStore
use handlers::*

#[tokio::main]
function main() -> Result<(), Box<dyn std::error::Error>> {
    // Setup logging
    env_logger::init()

    // Initialize data store
    let store = FileStore::new("./data".to_string())?

    println!("🚀 Starting Todo Management API server...")
    println!("📍 Server address: http://localhost:8080")
    println!("📋 API docs: http://localhost:8080/docs")

    // Start HTTP server
    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(store.clone()))
            .wrap(middleware::Logger::default())
            .wrap(middleware::DefaultHeaders::new()
                .header("Content-Type", "application/json; charset=utf-8"))
            .service(
                web::scope("/api/v1")
                    .route("/todos", web::get().to(getAllTodos))
                    .route("/todos", web::post().to(createTodo))
                    .route("/todos/{id}", web::get().to(getTodo))
                    .route("/todos/{id}", web::put().to(updateTodo))
                    .route("/todos/{id}", web::delete().to(deleteTodo))
                    .route("/todos/{id}/complete", web::patch().to(completeTodo))
                    .route("/todos/search", web::get().to(searchTodos))
            )
            .route("/health", web::get().to(healthCheck))
            .route("/docs", web::get().to(apiDocs))
    })
    .bind("127.0.0.1:8080")?
    .run()?

    Ok(())
}

Request Handlers

// src/handlers/todo_handlers.tpz
use web::{web, HttpRequest, HttpResponse, Result as WebResult}
use uuid::Uuid
use crate::models::{Todo, CreateTodoRequest, UpdateTodoRequest, TodoResponse, ApiResponse}
use crate::storage::FileStore

// Get all todos
function getAllTodos(
    store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
    match store.getAll() {
        case Ok(todos) => {
            let responseTodos: Array<TodoResponse> = todos
                .into_iter()
                .map(|todo| TodoResponse::from(todo))
                .collect()
            
            return Ok(HttpResponse::Ok()
                .json(ApiResponse::success(responseTodos)))
        }
        case Err(error) => {
            return Ok(HttpResponse::InternalServerError()
                .json(ApiResponse::<()>::error(error)))
        }
    }
}

// Create new todo
function createTodo(
    request: web::Json<CreateTodoRequest>,
    store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
    let mut newTodo = Todo::new(
        request.title.clone(),
        request.description.clone(),
        request.priority.unwrap_or(Priority::Normal)
    )

    // Add tags if provided
    match &request.tags {
        case Some(tagList) => {
            for tag in tagList {
                newTodo.addTag(tag.clone())
            }
        }
        case None => {}
    }

    match store.add(newTodo) {
        case Ok(createdTodo) => {
            return Ok(HttpResponse::Created()
                .json(ApiResponse::success(TodoResponse::from(createdTodo))))
        }
        case Err(error) => {
            return Ok(HttpResponse::InternalServerError()
                .json(ApiResponse::<()>::error(error)))
        }
    }
}

// Get specific todo
function getTodo(
    path: web::Path<Uuid>,
    store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
    let id = path.into_inner()

    match store.findById(id) {
        case Ok(Some(todo)) => {
            return Ok(HttpResponse::Ok()
                .json(ApiResponse::success(TodoResponse::from(todo))))
        }
        case Ok(None) => {
            return Ok(HttpResponse::NotFound()
                .json(ApiResponse::<()>::error("Todo not found".to_string())))
        }
        case Err(error) => {
            return Ok(HttpResponse::InternalServerError()
                .json(ApiResponse::<()>::error(error)))
        }
    }
}

// Update todo
function updateTodo(
    path: web::Path<Uuid>,
    request: web::Json<UpdateTodoRequest>,
    store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
    let id = path.into_inner()

    match store.findById(id) {
        case Ok(Some(mut existingTodo)) => {
            // Update only requested fields
            match &request.title {
                case Some(title) => {
                    existingTodo.title = title.clone()
                }
                case None => {}
            }
            match &request.description {
                case Some(description) => {
                    existingTodo.description = Some(description.clone())
                }
                case None => {}
            }
            match &request.priority {
                case Some(priority) => {
                    existingTodo.priority = priority.clone()
                }
                case None => {}
            }
            match &request.tags {
                case Some(tagList) => {
                    existingTodo.tags = tagList.clone()
                }
                case None => {}
            }

            match store.update(id, existingTodo) {
                case Ok(Some(updatedTodo)) => {
                    return Ok(HttpResponse::Ok()
                        .json(ApiResponse::success(TodoResponse::from(updatedTodo))))
                }
                case Ok(None) => {
                    return Ok(HttpResponse::NotFound()
                        .json(ApiResponse::<()>::error("Todo not found".to_string())))
                }
                case Err(error) => {
                    return Ok(HttpResponse::InternalServerError()
                        .json(ApiResponse::<()>::error(error)))
                }
            }
        }
        case Ok(None) => {
            return Ok(HttpResponse::NotFound()
                .json(ApiResponse::<()>::error("Todo not found".to_string())))
        }
        case Err(error) => {
            return Ok(HttpResponse::InternalServerError()
                .json(ApiResponse::<()>::error(error)))
        }
    }
}

// Delete todo
function deleteTodo(
    path: web::Path<Uuid>,
    store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
    let id = path.into_inner()

    match store.delete(id) {
        case Ok(true) => {
            return Ok(HttpResponse::Ok()
                .json(ApiResponse::success("Todo deleted successfully".to_string())))
        }
        case Ok(false) => {
            return Ok(HttpResponse::NotFound()
                .json(ApiResponse::<()>::error("Todo not found".to_string())))
        }
        case Err(error) => {
            return Ok(HttpResponse::InternalServerError()
                .json(ApiResponse::<()>::error(error)))
        }
    }
}

// Complete todo
function completeTodo(
    path: web::Path<Uuid>,
    store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
    let id = path.into_inner()

    match store.findById(id) {
        case Ok(Some(mut todo)) => {
            todo.complete()
            
            match store.update(id, todo) {
                case Ok(Some(completedTodo)) => {
                    return Ok(HttpResponse::Ok()
                        .json(ApiResponse::success(TodoResponse::from(completedTodo))))
                }
                case Err(error) => {
                    return Ok(HttpResponse::InternalServerError()
                        .json(ApiResponse::<()>::error(error)))
                }
                case _ => {
                    return Ok(HttpResponse::InternalServerError()
                        .json(ApiResponse::<()>::error("Failed to complete todo".to_string())))
                }
            }
        }
        case Ok(None) => {
            return Ok(HttpResponse::NotFound()
                .json(ApiResponse::<()>::error("Todo not found".to_string())))
        }
        case Err(error) => {
            return Ok(HttpResponse::InternalServerError()
                .json(ApiResponse::<()>::error(error)))
        }
    }
}

// Search todos (using query parameters)
function searchTodos(
    request: HttpRequest,
    store: web::Data<FileStore>
) -> WebResult<HttpResponse> {
    let query = web::Query::<HashMap<String, String>>::from_query(request.query_string())
        .map_err(|_| HttpResponse::BadRequest().json(ApiResponse::<()>::error("Invalid query parameters".to_string())))?

    let todos = match store.getAll() {
        case Ok(list) => list
        case Err(error) => {
            return Ok(HttpResponse::InternalServerError()
                .json(ApiResponse::<()>::error(error)))
        }
    }

    let mut filteredTodos = todos

    // Filter by completion status
    match query.get("completed") {
        case Some(completedStr) => {
            let completedStatus = completedStr.parse::<bool>().unwrap_or(false)
            filteredTodos = filteredTodos.into_iter()
                .filter(|todo| todo.completed == completedStatus)
                .collect()
        }
        case None => {}
    }

    // Filter by priority
    match query.get("priority") {
        case Some(priorityStr) => {
            match priorityStr.parse::<Priority>() {
                case Ok(priority) => {
                    filteredTodos = filteredTodos.into_iter()
                        .filter(|todo| todo.priority == priority)
                        .collect()
                }
                case Err(_) => {}
            }
        }
        case None => {}
    }

    // Filter by tag
    match query.get("tag") {
        case Some(tag) => {
            filteredTodos = filteredTodos.into_iter()
                .filter(|todo| todo.tags.contains(tag))
                .collect()
        }
        case None => {}
    }

    // Search in title
    match query.get("search") {
        case Some(searchTerm) => {
            let lowercaseSearch = searchTerm.to_lowercase()
            filteredTodos = filteredTodos.into_iter()
                .filter(|todo| todo.title.to_lowercase().contains(&lowercaseSearch))
                .collect()
        }
        case None => {}
    }

    // Sort by priority
    filteredTodos.sort_by(|a, b| b.priorityScore().cmp(&a.priorityScore()))

    let responseTodos: Array<TodoResponse> = filteredTodos
        .into_iter()
        .map(|todo| TodoResponse::from(todo))
        .collect()

    return Ok(HttpResponse::Ok()
        .json(ApiResponse::success(responseTodos)))
}

// Health check
function healthCheck() -> WebResult<HttpResponse> {
    return Ok(HttpResponse::Ok()
        .json(json!({
            "status": "healthy",
            "timestamp": chrono::Utc::now().to_rfc3339(),
            "version": "0.1.0"
        })))
}

// API documentation
function apiDocs() -> WebResult<HttpResponse> {
    let docs = r#"
    <!DOCTYPE html>
    <html>
    <head>
        <title>Todo Management API Documentation</title>
        <meta charset="utf-8">
        <style>
            body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 40px; }
            .endpoint { background: #f8f9fa; padding: 15px; margin: 10px 0; border-radius: 5px; }
            .method { font-weight: bold; padding: 3px 8px; border-radius: 3px; color: white; }
            .get { background: #28a745; }
            .post { background: #007bff; }
            .put { background: #ffc107; color: black; }
            .delete { background: #dc3545; }
            .patch { background: #6f42c1; }
        </style>
    </head>
    <body>
        <h1>🚀 Todo Management API Documentation</h1>
        
        <div class="endpoint">
            <span class="method get">GET</span>
            <strong>/api/v1/todos</strong>
            <p>Get all todos.</p>
        </div>
        
        <div class="endpoint">
            <span class="method post">POST</span>
            <strong>/api/v1/todos</strong>
            <p>Create a new todo.</p>
            <pre>{ "title": "string", "description": "string?", "priority": "enum?", "tags": "string[]?" }</pre>
        </div>
        
        <div class="endpoint">
            <span class="method get">GET</span>
            <strong>/api/v1/todos/{id}</strong>
            <p>Get a specific todo.</p>
        </div>
        
        <div class="endpoint">
            <span class="method put">PUT</span>
            <strong>/api/v1/todos/{id}</strong>
            <p>Update a todo.</p>
        </div>
        
        <div class="endpoint">
            <span class="method delete">DELETE</span>
            <strong>/api/v1/todos/{id}</strong>
            <p>Delete a todo.</p>
        </div>
        
        <div class="endpoint">
            <span class="method patch">PATCH</span>
            <strong>/api/v1/todos/{id}/complete</strong>
            <p>Mark a todo as completed.</p>
        </div>
        
        <div class="endpoint">
            <span class="method get">GET</span>
            <strong>/api/v1/todos/search</strong>
            <p>Search todos. Query parameters: completed, priority, tag, search</p>
        </div>
        
        <h2>🏥 Health Check</h2>
        <div class="endpoint">
            <span class="method get">GET</span>
            <strong>/health</strong>
            <p>Check server status.</p>
        </div>
    </body>
    </html>
    "#;

    return Ok(HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(docs))
}

🧪 Writing Tests

Integration Tests

// tests/integration_test.tpz
use actix_web::{test, App}
use serde_json::json
use tempfile::tempdir
use TodoAPI::*

#[actix_rt::test]
function todo_crud_test() -> Result<(), Box<dyn std::error::Error>> {
    // Setup temporary storage
    let tempDir = tempdir().unwrap()
    let store = FileStore::new(tempDir.path().to_str().unwrap().to_string()).unwrap()

    let mut app = test::init_service(
        App::new()
            .app_data(web::Data::new(store))
            .service(
                web::scope("/api/v1")
                    .route("/todos", web::get().to(getAllTodos))
                    .route("/todos", web::post().to(createTodo))
                    .route("/todos/{id}", web::get().to(getTodo))
                    .route("/todos/{id}", web::put().to(updateTodo))
                    .route("/todos/{id}", web::delete().to(deleteTodo))
            )
    )?

    // 1. Create todo test
    let newTodoRequest = json!({
        "title": "Complete Topaz Project",
        "description": "Finish the first Topaz project",
        "priority": "High",
        "tags": ["development", "learning"]
    })

    let req = test::TestRequest::post()
        .uri("/api/v1/todos")
        .set_json(&newTodoRequest)
        .to_request()

    let resp = test::call_service(&mut app, req)?
    assert!(resp.status().is_success())

    let respBody: serde_json::Value = test::read_body_json(resp)?
    assert!(respBody["success"].as_bool().unwrap())
    
    let createdTodo = &respBody["data"]
    let todoId = createdTodo["id"].as_str().unwrap()

    // 2. Get todo test
    let req = test::TestRequest::get()
        .uri(&format!("/api/v1/todos/{}", todoId))
        .to_request()

    let resp = test::call_service(&mut app, req)?
    assert!(resp.status().is_success())

    let respBody: serde_json::Value = test::read_body_json(resp)?
    let retrievedTodo = &respBody["data"]
    assert_eq!(retrievedTodo["title"], "Complete Topaz Project")

    // 3. Get all todos test
    let req = test::TestRequest::get()
        .uri("/api/v1/todos")
        .to_request()

    let resp = test::call_service(&mut app, req)?
    assert!(resp.status().is_success())

    let respBody: serde_json::Value = test::read_body_json(resp)?
    let todoList = respBody["data"].as_array().unwrap()
    assert_eq!(todoList.len(), 1)

    // 4. Update todo test
    let updateRequest = json!({
        "title": "Complete Topaz Project (Updated)",
        "priority": "Urgent"
    })

    let req = test::TestRequest::put()
        .uri(&format!("/api/v1/todos/{}", todoId))
        .set_json(&updateRequest)
        .to_request()

    let resp = test::call_service(&mut app, req)?
    assert!(resp.status().is_success())

    // 5. Delete todo test
    let req = test::TestRequest::delete()
        .uri(&format!("/api/v1/todos/{}", todoId))
        .to_request()

    let resp = test::call_service(&mut app, req)?
    assert!(resp.status().is_success())

    // 6. Verify deletion
    let req = test::TestRequest::get()
        .uri(&format!("/api/v1/todos/{}", todoId))
        .to_request()

    let resp = test::call_service(&mut app, req)?
    assert_eq!(resp.status(), 404)

    Ok(())
}

#[actix_rt::test]
function todo_search_test() -> Result<(), Box<dyn std::error::Error>> {
    // Test data setup and search functionality testing
    // ... (search-related test code)

    Ok(())
}

🚀 Running and Testing

Run Project

# Install dependencies
topaz build

# Run development server
topaz run

# Or run in release mode
topaz run --release

API Testing

# Health check
curl http://localhost:8080/health

# Create new todo
curl -X POST http://localhost:8080/api/v1/todos \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Learn Topaz",
    "description": "Learn all features of Topaz language",
    "priority": "High",
    "tags": ["learning", "programming"]
  }'

# Get all todos
curl http://localhost:8080/api/v1/todos

# Search incomplete todos
curl "http://localhost:8080/api/v1/todos/search?completed=false"

# Search high priority todos
curl "http://localhost:8080/api/v1/todos/search?priority=High"

Run Tests

# Run all tests
topaz test

# Run specific test
topaz test integration_test

# Check test coverage
topaz test --coverage

🎯 Next Steps

Congratulations! You've completed your first complete Topaz project! 🎉

Additional features to implement:

  1. Database Integration: PostgreSQL, MongoDB, etc.
  2. User Authentication: JWT token-based authentication
  3. Real-time Updates: Real-time todo synchronization via WebSocket
  4. File Upload: File attachment feature for todos
  5. Email Notifications: Deadline reminder notifications
  6. Docker Deployment: Containerization and cloud deployment

Core Topaz concepts you've learned:

  • ✅ Project structure design
  • ✅ Data modeling and type system
  • ✅ Error handling (Result/Option types)
  • ✅ Web server and REST API construction
  • ✅ File-based data storage
  • ✅ Pattern matching utilization
  • ✅ Test-driven development
  • ✅ Multi-language identifiers and natural code writing

You're now ready to tackle more complex Topaz projects! 🚀