🎯

rust-backend

🎯Skill

from windmill-labs/windmill

VibeIndex|
What it does

Enforces structured, idiomatic Rust coding patterns for Windmill's backend, focusing on type safety, functional programming, and clean error handling.

πŸ“¦

Part of

windmill-labs/windmill(3 items)

rust-backend

Installation

npm runRun npm script
npm run dev
CargoRun with Cargo (Rust)
cargo install sqlx-cli
πŸ“– Extracted from docs: windmill-labs/windmill
4Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

Rust coding guidelines for the Windmill backend. MUST use when writing or modifying Rust code in the backend directory.

Overview

# Rust Backend Coding Guidelines

Apply these patterns when writing or modifying Rust code in the backend/ directory.

Data Structure Design

Choose between struct, enum, or newtype based on domain needs:

  • Use enum for state machines instead of boolean flags or loosely related fields
  • Model invariants explicitly using types (e.g., NonZeroU32, Duration, custom enums)
  • Consider ownership of each field:

- Use &str vs String, slices vs vectors

- Use Arc when sharing across threads

- Use Cow<'a, T> for flexible ownership

```rust

// State machine with enum

enum JobState {

Pending { scheduled_for: DateTime },

Running { started_at: DateTime, worker: String },

Completed { result: JobResult, duration_ms: i64 },

Failed { error: String, retries: u32 },

}

// Avoid multiple booleans

struct Job {

is_pending: bool, // Don't do this

is_running: bool,

is_completed: bool,

}

```

Impl Block Organization

Place impl blocks immediately below the struct/enum they modify. Group methods logically:

```rust

struct JobQueue {

jobs: Vec,

capacity: usize,

}

impl JobQueue {

// Constructors first

pub fn new(capacity: usize) -> Self { ... }

pub fn with_jobs(jobs: Vec) -> Self { ... }

// Getters

pub fn len(&self) -> usize { ... }

pub fn is_empty(&self) -> bool { ... }

// Mutation methods

pub fn push(&mut self, job: Job) -> Result<()> { ... }

pub fn pop(&mut self) -> Option { ... }

// Domain logic

pub fn next_scheduled(&self) -> Option<&Job> { ... }

}

```

Iterator Chains Over For-Loops

Prefer functional iterator chains (.filter().map().collect()) over imperative for-loops:

```rust

// Preferred

let results: Vec<_> = items

.iter()

.filter(|item| item.is_valid())

.map(|item| item.transform())

.collect();

// Avoid

let mut results = Vec::new();

for item in items.iter() {

if item.is_valid() {

results.push(item.transform());

}

}

```

Error Handling

Use the Error type from windmill_common::error. Return Result or JsonResult for fallible functions:

```rust

use windmill_common::error::{Error, Result};

// Use ? operator for propagation

pub async fn get_job(db: &DB, id: Uuid) -> Result {

let job = sqlx::query_as!(Job, "SELECT ... WHERE id = $1", id)

.fetch_optional(db)

.await?

.ok_or_else(|| Error::NotFound("job not found".to_string()))?;

Ok(job)

}

```

Prefer if let for optional handling. Use let...else when early return makes code clearer:

```rust

let Some(config) = get_config() else {

return Err(Error::MissingConfig);

};

```

Never panic in library code. Reserve .unwrap() for cases with compile-time guarantees. Keep functions short to help lifetime inference and clarity.

Early Returns

Return early to avoid deep nesting. Handle error cases and edge conditions first:

```rust

// Preferred - early returns

fn process_job(job: Option) -> Result {

let Some(job) = job else {

return Ok(Output::default());

};

if !job.is_valid() {

return Err(Error::InvalidJob);

}

if job.is_cached() {

return Ok(job.cached_result());

}

// Main logic at the end, not nested

execute_job(job)

}

// Avoid - deep nesting

fn process_job(job: Option) -> Result {

if let Some(job) = job {

if job.is_valid() {

if !job.is_cached() {

execute_job(job)

} else {

Ok(job.cached_result())

}

} else {

Err(Error::InvalidJob)

}

} else {

Ok(Output::default())

}

}

```

Variable Shadowing

Shadow variables instead of creating new names with prefixes:

```rust

// Preferred

let data = fetch_raw_data();

let data = parse(data);

let data = validate(data)?;

// Avoid

let raw_data = fetch_raw_data();

let parsed_data = parse(raw_data);

let validated_data = validate(parsed_data)?;

```

Minimal Comments

  • No inline comments explaining obvious code
  • No TODO/FIXME comments in committed code
  • Doc comments (///) only on public items
  • Let code be self-documenting through clear naming

Type Safety

Use enums over boolean flags for clarity:

```rust

// Preferred

enum JobStatus {

Pending,

Running,

Completed,

}

// Avoid

struct Job {

is_running: bool,

is_completed: bool,

}

```

Pattern Matching

Prefer explicit matching. Use wildcards strategically for fallback cases or ignored fields:

```rust

// Explicit matching preferred

match status {

JobStatus::Pending => handle_pending(),

JobStatus::Running => handle_running(),

JobStatus::Completed => handle_completed(),

}

// Wildcards OK for fallback

match result {

Ok(value) => process(value),

Err(_) => return default_value(),

}

// Wildcards OK for ignoring fields in destructuring

let Point { x, y, .. } = point;

```

Destructuring in Function Signatures

Destructure structs directly in function parameters:

```rust

// Preferred

async fn process_job(

Extension(db): Extension,

Path((workspace, job_id)): Path<(String, Uuid)>,

Query(pagination): Query,

) -> Result> {

// ...

}

// Avoid

async fn process_job(

db_ext: Extension,

path: Path<(String, Uuid)>,

query: Query,

) -> Result> {

let Extension(db) = db_ext;

let Path((workspace, job_id)) = path;

// ...

}

```

Trait Implementations

Use standard trait implementations to simplify conversions and reduce boilerplate:

```rust

// Implement From/Into for type conversions

impl From for ApiJob {

fn from(db: DbJob) -> Self {

ApiJob {

id: db.id,

status: db.status.into(),

}

}

}

// Use TryFrom for fallible conversions

impl TryFrom for JobKind {

type Error = Error;

fn try_from(s: String) -> Result { ... }

}

```

Apply derive macros to reduce boilerplate:

```rust

#[derive(Debug, Clone, Serialize, Deserialize)]

pub struct Job { ... }

```

Module Structure

  • Use pub(crate) instead of pub when possible; expose only what needs exposing
  • Keep APIs small and expressive; avoid leaking internal types
  • Organize code into modules reflecting ownership and domain boundaries

```rust

// Prefer restricted visibility

pub(crate) fn internal_helper() { ... }

// Only pub for external API

pub fn create_job(...) -> Result { ... }

```

Code Navigation

Always use rust-analyzer LSP for:

  • Go to definition
  • Find references
  • Type information
  • Import resolution

Do not guess at module paths or type definitions.

JSON Handling

Prefer Box over serde_json::Value when:

  • Storing JSON in the database (JSONB columns)
  • Passing JSON through without modification
  • The JSON structure doesn't need inspection

```rust

// Preferred - avoids parsing/serialization overhead

pub struct Job {

pub id: Uuid,

pub args: Option>,

}

// Only use Value when you need to inspect/modify JSON

let value: serde_json::Value = serde_json::from_str(&json)?;

if let Some(field) = value.get("field") {

// modify or inspect

}

```

Serde Optimizations

Use serde attributes to optimize serialization:

```rust

#[derive(Serialize, Deserialize)]

pub struct Job {

#[serde(rename = "jobId")]

pub id: Uuid,

#[serde(default)]

pub priority: i32,

#[serde(skip_serializing_if = "Option::is_none")]

pub parent_job: Option,

#[serde(skip_serializing_if = "Vec::is_empty")]

pub tags: Vec,

}

```

Prefer borrowing for zero-copy deserialization when lifetimes allow:

```rust

#[derive(Deserialize)]

pub struct JobInput<'a> {

#[serde(borrow)]

pub workspace_id: Cow<'a, str>,

#[serde(borrow)]

pub script_path: &'a str,

}

```

SQLx Patterns

*Never use SELECT ** - always list columns explicitly. This is critical for backwards compatibility when workers run behind the API server version:

```rust

// Preferred - explicit columns

sqlx::query_as!(

Job,

"SELECT id, workspace_id, path, created_at FROM v2_job WHERE id = $1",

job_id

)

// Avoid - breaks when columns are added

sqlx::query_as!(Job, "SELECT * FROM v2_job WHERE id = $1", job_id)

```

Use batch operations to minimize round trips:

```rust

// Preferred - single query with multiple values

sqlx::query!(

"INSERT INTO job_logs (job_id, logs) VALUES ($1, $2), ($3, $4)",

id1, log1, id2, log2

)

// Avoid N+1 queries

for id in ids {

sqlx::query!("SELECT ... WHERE id = $1", id).fetch_one(db).await?;

}

// Preferred - single query with IN clause

sqlx::query!("SELECT ... WHERE id = ANY($1)", &ids[..]).fetch_all(db).await?

```

Use transactions for multi-step operations and parameterize all queries.

Async & Tokio Patterns

Never block the async runtime. Use spawn_blocking for CPU-intensive or blocking I/O:

```rust

// Preferred - offload blocking work

let result = tokio::task::spawn_blocking(move || {

expensive_computation(&data)

}).await?;

// Avoid - blocks the runtime

let result = expensive_computation(&data); // Don't do this in async

```

Use tokio primitives for sleep and channels:

```rust

use tokio::sync::mpsc;

use tokio::time::sleep;

// Avoid in async contexts

use std::thread::sleep; // Blocks the runtime

```

Use bounded channels for backpressure:

```rust

// Preferred - bounded channel prevents overwhelming

let (tx, rx) = tokio::sync::mpsc::channel(100);

// Be careful with unbounded

let (tx, rx) = tokio::sync::mpsc::unbounded_channel();

```

Mutex Selection in Async Code

Prefer std::sync::Mutex (or parking_lot::Mutex) over tokio::sync::Mutex for protecting data in async code. The async mutex is more expensive and only needed when holding locks across .await points.

```rust

// Preferred for data protection - std mutex is faster

use std::sync::Mutex;

struct Cache {

data: Mutex>,

}

impl Cache {

fn get(&self, key: &str) -> Option {

self.data.lock().unwrap().get(key).cloned()

}

fn insert(&self, key: String, value: Value) {

self.data.lock().unwrap().insert(key, value);

}

}

```

Use tokio::sync::Mutex only when you must hold the lock across .await points, typically for IO resources like database connections:

```rust

use tokio::sync::Mutex;

use std::sync::Arc;

// Async mutex for IO resources held across await points

let conn = Arc::new(Mutex::new(db_connection));

async fn execute_query(conn: Arc>, query: &str) {

let mut lock = conn.lock().await;

lock.execute(query).await; // Lock held across .await

}

```

Common pattern: Wrap Arc> in a struct with non-async methods that lock internally, keeping lock scope minimal:

```rust

struct SharedState {

inner: std::sync::Mutex,

}

impl SharedState {

fn update(&self, value: i32) {

self.inner.lock().unwrap().value = value;

}

fn get(&self) -> i32 {

self.inner.lock().unwrap().value

}

}

```

Alternative for IO resources: Spawn a dedicated task to manage the resource and communicate via message passing:

```rust

let (tx, mut rx) = tokio::sync::mpsc::channel(32);

tokio::spawn(async move {

while let Some(cmd) = rx.recv().await {

handle_io_command(&mut resource, cmd).await;

}

});

```

Build & Tooling

Build speed tips:

  • Use cargo check during rapid iteration over cargo build
  • Minimize unnecessary dependencies and feature flags