| name | my-rust-standards |
| description | Project-specific Rust engineering standards for the tider-api-proxy server. Use this skill when: (1) writing or refactoring any Rust code in this workspace, (2) reviewing a PR or diff for style, structure, or naming issues, (3) adding new modules or public API surface, (4) writing or renaming test functions, (5) deciding whether a helper function or wrapper is justified.
|
| license | MIT |
| compatibility | Rust stable, Cargo workspace |
| metadata | {"author":"tider","version":"1.0.0"} |
| allowed-tools | Bash(cargo:*) Bash(rustfmt:*) Bash(clippy:*) Read Write Edit Glob Grep |
Tider API Proxy — Rust Engineering Standards
Apply these rules whenever writing, reviewing, or refactoring Rust code in this workspace. They
extend the project's CLAUDE.md and take precedence over general Rust style where they conflict.
Role
Act as a senior Rust engineer. The goal is idiomatic, readable, maintainable code — not
clever code. When in doubt, choose the boring, obvious solution.
Code Style
Formatting and linting
-
All code must pass rustfmt without manual overrides.
-
All code must pass cargo clippy -- -D warnings. Fix the root cause; do not suppress with
#[allow(...)] unless the lint is genuinely wrong for this case (add a comment explaining why).
-
Prefer #[expect(clippy::lint)] over #[allow(...)] — it fails if the lint disappears.
-
Never use anyhow (or any erased/dynamic error type) in library or production code.
Every fallible operation defines and returns a concrete error type derived from thiserror.
-
Define errors with thiserror. Use the pattern FooError / FooResult<T>. One error enum per
domain boundary; an app/handler-level enum aggregates them with #[from].
-
Return Result<T, E> for all fallible operations.
-
Propagate errors implicitly with ?, not map_err. Wire conversions through
#[from] on the error enum so ? converts automatically. Reserve map_err only for the rare
case where the source error is not Send + Sync (e.g. it must be flattened to a String
field) and #[from] is therefore impossible — and add a one-line comment saying so.
-
Never use unwrap() or expect() in production code paths. Reserve them for tests and
main() where a panic is acceptable.
-
Do not swallow errors with let _ = ... unless the discard is intentional and obvious.
Ownership and borrowing
- Prefer
&T over .clone() when ownership is not needed.
- Use
&str / &[T] in function parameters; return String / Vec<T> when ownership is given.
- Small
Copy types (≤ 32 bytes) may be passed by value.
Project Structure
Group by responsibility, not by kind
Modules must reflect a domain concept or adapter boundary, not a category of language construct.
// Bad
utils.rs
helpers.rs
types.rs
// Good
server.rs // Axum router, RPC dispatch, request handling
client.rs // Outbound HTTP client, upstream forwarding
auth.rs // Bearer token validation
shared.rs // Types shared across module boundaries
Accepted top-level groupings
| Group | What belongs there |
|---|
src/ root | One file per domain boundary (server, client, auth, shared) |
tests/ | Integration tests that exercise the full proxy pipeline |
Module files
- Every module must justify its existence. A file that only re-exports from one other file is
indirection with no value — merge or delete it.
mod.rs is acceptable for directories with multiple submodules. Single-file modules use
module_name.rs directly.
Naming
Files and modules
snake_case only.
- Name by responsibility, not by the Rust construct inside:
rpc_handler.rs not handler_impl.rs, upstream_client.rs not client.rs.
Types and functions
| Kind | Convention | Example |
|---|
| Error type | FooError | ProxyError, AuthError |
| Result alias | FooResult<T> | ProxyResult<T> |
| Entry-point function | verb + noun | run_proxy_server, handle_rpc_request |
| Builder method | with_foo | with_api_key, with_timeout |
| Predicate | is_ / has_ | is_authenticated, has_upstream |
| Conversion | into_ / as_ | into_response, as_bytes |
Functions
Each function must do one thing
A function whose entire body is a single delegating call to another function adds indirection
without value. Remove it and let callers call the target directly, or make the field pub.
fn get_state(&self) -> Arc<AppState> { self.state.clone() }
pub state: Arc<AppState>
Exceptions: trait implementations that must exist by contract (e.g. impl IntoResponse), and
builder methods that enforce an invariant beyond the assignment.
No shallow wrappers in request paths
In hot paths (RPC dispatch, upstream forwarding, auth validation), every function call must be
justified by logic it owns — routing, decoding, error translation, etc. A function that only
calls inner.forward(...) and returns the result should be inlined.
Tests
Naming — maximum 4 words total
Test function names must be short and specific. 4 words is the hard limit for the whole
name (the optional test_ prefix does not count as a word).
fn test_rpc_handler_returns_error_when_upstream_is_unreachable()
fn auth_middleware_rejects_request_with_invalid_bearer_token()
fn upstream_unreachable_errors()
fn rejects_invalid_bearer()
fn encode_wav_riff_header()
fn empty_tokens_error()
Name format: <subject>_<condition> or <subject>_<result>. Each word should be a single token
or well-known abbreviation (rpc, tts, auth).
Structure
- One logical assertion per test. Use multiple
assert_* calls only when they verify the same
invariant from different angles.
- Do not test implementation details — test observable outputs.
#[should_panic] tests must include an expected = "…" substring to avoid masking unrelated panics.
Placement
- Unit tests go in a
#[cfg(test)] mod tests { … } block at the bottom of the file they test.
- Integration tests that exercise the full proxy pipeline go in
tests/integration.rs.
- Tests that require a live upstream (tts-rs or S3) must be
#[ignore] and documented with a
one-line setup comment.
Comments
Only write a comment when the why is non-obvious. Never write a comment that restates what
the code already says.
req.headers_mut().insert(AUTHORIZATION, token);
let claims = auth::verify(&key)?;
headers.insert("fly-force-instance-id", instance_id);
Section dividers (// ── Foo ──────────) are forbidden. Use blank lines and module structure
to group code instead.
Verification
After any refactor or new code:
cargo fmt --all -- --check
cargo clippy -- -D warnings
cargo test
All three must pass before the work is complete.