Skip to content

Latest commit

 

History

History
104 lines (83 loc) · 3.54 KB

File metadata and controls

104 lines (83 loc) · 3.54 KB

1. Design The Contract

Start with workflows, not with Axum routes or database tables. Write down what clients need to do and which operations need identity.

For the team workspace, the first pass looks like this:

Workflow Macro Reason
list projects, read tasks, create tasks rest_service! conventional JSON resources, OpenAPI, browser clients
upload and download task attachments file_service! streaming, multipart validation, early auth checks
live task activity jsonrpc_bidirectional_service! typed WebSocket notifications
command-heavy workflows jsonrpc_service! optional alternative for RPC-style APIs

Name Permissions Early

Permissions should be stable application concepts, not incidental handler details. Good permission names usually describe the capability:

project:read
project:write
task:write
attachment:read
attachment:write
admin

Each protected operation declares those requirements in the API definition:

GET WITH_PERMISSIONS(["project:read"]) projects() -> ProjectsResponse,
POST WITH_PERMISSIONS(["task:write"]) projects/{project_id: String}/tasks(CreateTaskRequest) -> Task,
DELETE WITH_PERMISSIONS(["admin"] | ["project:owner"]) projects/{project_id: String}() -> (),

WITH_PERMISSIONS(["a", "b"]) means the authenticated user needs both permissions. WITH_PERMISSIONS(["a"] | ["b", "c"]) means either the first group or the second group is enough. WITH_PERMISSIONS([]) means authenticated, with no extra permission requirement.

Keep DTOs Boring

DTOs should be explicit, serializable, and independent of storage models. Avoid exposing database-specific fields just because they exist.

#[cfg(feature = "server")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "server", derive(JsonSchema))]
pub struct Task {
    pub id: String,
    pub project_id: String,
    pub title: String,
    pub status: TaskStatus,
    pub assignee_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "server", derive(JsonSchema))]
pub enum TaskStatus {
    Open,
    InProgress,
    Done,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "server", derive(JsonSchema))]
pub struct CreateTaskRequest {
    pub title: String,
    pub assignee_id: Option<String>,
}

The JsonSchema derive is gated because only server/spec generation needs it. Shared serialization stays available with no transport feature enabled.

Sketch The Service

A REST task service definition can stay close to the client workflow:

rest_service!({
    service_name: TaskService,
    base_path: "/api/v1",
    openapi: true,
    serve_docs: true,
    docs_path: "/docs",
    endpoints: [
        GET WITH_PERMISSIONS(["project:read"]) projects() -> ProjectsResponse,
        GET WITH_PERMISSIONS(["project:read"]) projects/{project_id: String}/tasks() -> TasksResponse,
        POST WITH_PERMISSIONS(["task:write"]) projects/{project_id: String}/tasks(CreateTaskRequest) -> Task,
        PATCH WITH_PERMISSIONS(["task:write"]) tasks/{task_id: String}(UpdateTaskRequest) -> Task,
    ]
});

At this point you have made the most important design decisions: operation names, path parameters, request/response types, and auth requirements. The server implementation can change later without changing this contract.