rust-api-design について
Rust API Design は、Rust API Guidelines に基づいて Rust の公開 crate API を設計するスキルです。crate 作者が使いやすく予測可能で型安全なインターフェースを作り、将来の変更余地を確保するのに役立ちます。
主な機能
- 01命名と変換の規約
- 02型、トレイト、ジェネリクスの選択
- 03newtype と型安全な引数
- 04イテレーターとトレイトの設計
- 05API の将来の変更への備え
活用シーン
- 01公開 crate API のレビュー
- 02変換メソッドやアクセサーの設計
- 03曖昧な引数をドメイン型に置き換える
- 04将来の変更に備えた公開インターフェースの設計
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ full-stack-skills/ rust-skills --skill rust-api-designインストール後、Claude Code で/rust-api-designを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlexamples/anti-patterns.mdexamples/golden-api/Cargo.lockCargo.tomlexamples/golden-api/src/lib.rsreferences/api-guidelines-checklist.mdfuture-proofing.mdnaming-and-conversions.md詳細
Rust API 設計(Rust API ガイドライン)
権威ある情報源:Rust API Guidelines — std、tokio、serde、bevy で採用されている、約 100 項目の C-* ルールからなる事実上の標準チェックリストです。各ルールの正式な文言については、完全なチェックリストを参照してください。
このスキルが対象とするのは、公開 Rust API の形状です。具体的には、型、トレイト、命名、変換、そして API の使いやすさ、扱いやすさ、破壊変更への耐性を保つ境界です。Cargo マニフェスト(rust-cargo-build)、ワークスペースの構成(rust-workspace)、クレート内の src/ 構成(rust-module-layout)、semver/公開(rust-semver)は対象外です。
対応範囲
✅ 得意分野
- API Guidelines の約 100 項目の C-* ルールを、公開クレートの具体的な設計判断に落とし込む
- std およびエコシステムの慣習に合わせて、型、関数、メソッド、フィーチャーに名前を付ける
- ジェネリクス、トレイトオブジェクト、具体型、newtype パターンから適切なものを選ぶ
From/Into/TryFrom/AsRef/Borrowの変換を設計する(Derefによるポリモーフィズムは避ける)- どの自動トレイト(
Debug、Clone、Eq、Hash、Send、Sync)を導出するか選ぶ - トレイトをシールし、
#[non_exhaustive]を付け、構造体のフィールドを非公開にして、将来の拡張余地を確保する - bool パラメーターを enum に、数値を newtype に、
Stringを&strに置き換える - イテレーター、
Extend、Default、Display/FromStrを適切に設計する - 公開 API での
panic!/unwrap/expectを避け、代わりにResultを使う
⚠️ 前提条件
- Rust の所有権、トレイト、ライフタイム —
rust-stableを参照 - モジュールの可視性と再エクスポート —
rust-module-layoutを参照 [features]とoptional = trueを設定する Cargo マニフェスト —rust-cargo-buildを参照
❌ 対象外
- semver のルールと公開ワークフロー →
rust-semver - Clippy の lint 設定 →
rust-style-clippy - doctest と rustdoc コメント →
rust-documentation - ワークスペースの構成 →
rust-workspace
データのプライバシー
このスキルはユーザーデータを収集、保存、送信しません。
8 つの章(API Guidelines の各章に対応するセクション)
Guidelines は 11 章で構成されています。このスキルが対象とするのは、設計に関する 8 章です。ドキュメント、マクロ、必須事項は、それぞれ rust-documentation、rust-macros、rust-cargo-build が対象とします。
1. 命名(C-CASE、C-CONV、C-GETTER、C-NAMING)
C-CASE — 大文字・小文字の慣習
| 項目 | ケース | 例 |
|---|---|---|
| 型(struct/enum/trait)、モジュール、クレート | UpperCamelCase | HttpClient、tokio |
| 関数、メソッド、ローカル変数、フィールド | snake_case | send_request |
| 定数、static 変数 | SCREAMING_SNAKE_CASE | MAX_RETRIES |
| ジェネリック型 | 1 文字の UpperCamelCase、または短い単語 | T、K、V、Req |
| ライフタイム | 短い 'a/'b、または説明的な 'src | 'src |
| フィーチャー | kebab-case(Cargo が強制) | serde-json |
場当たり的な略語は避けてください。BufferedReader は std の規約に準拠していますが、BufRdr は準拠していません。
C-CONV — 変換メソッドの命名
| 変換 | 接頭辞 | 借用するか | 例 |
|---|---|---|---|
| 低コスト、借用 | as_ | はい(&self → &T) | as_slice、as_bytes |
| 低コスト、所有 | to_(確保なし)/into_(self を消費) | 場合による | to_vec、into_bytes |
| 高コスト、新しい値を返す | to_ | いいえ(&self → T) | to_lowercase |
| 消費する | into_ | いいえ(self → T) | into_string、into_iter |
| 失敗する可能性がある | TryFrom/TryInto | — | u32::try_from(byte) |
要件:(1) as_X という名前で所有値の X を返すメソッドは誤りです。to_X に変更してください。(2) 可変借用に対応する逆向きの名前は as_X_mut/X_mut です。
C-GETTER — アクセサーの命名
// ✅ C-GETTER compliant
pub struct Buffer { data: Vec<u8> }
impl Buffer {
pub fn data(&self) -> &[u8] { &self.data } // no get_ prefix
pub fn data_mut(&mut self) -> &mut [u8] { &mut self.data }
pub fn len(&self) -> usize { self.data.len() } // not get_len
pub fn is_empty(&self) -> bool { self.data.is_empty() }
}
get_ が許容される例外:Cell::get、Map::get(実際の検索を行う場合)。
2. 相互運用性(C-COMMON-TRAITS、C-CONVERT、C-ITER、C-SERDE)
C-COMMON-TRAITS — 明らかなトレイトを導出する
すべての公開型について、Debug、Clone、PartialEq、Eq、PartialOrd、Ord、Hash、Defaultを導出すべきか検討します。
| トレイト | デフォルト | 例外 |
|---|---|---|
Debug | はい | シークレットの場合は、値を隠す手動のDebug実装にします |
Clone | 安価ならはい | 複製にコストがかかる場合は、省略してその旨を文書化します |
Copy | 16バイト以下かつ損失がない場合のみ | — |
PartialEq/Eq | 全順序が成立する場合ははい | f64はEqにできません |
Hash | Eqの場合ははい | Eqとの整合性が必要です |
Default | 自然な空またはゼロの値がある場合ははい | — |
C-CONVERT — From/Into/AsRef/Borrowを使い、Derefによる多相性は避ける
// ✅ Impl From<T> for U; Into comes for free
impl From<ErrorKind> for Error { fn from(k: ErrorKind) -> Self { Error::Kind(k) } }
// ✅ AsRef for borrowed views
impl AsRef<str> for Name { fn as_ref(&self) -> &str { &self.0 } }
// ✅ Borrow<str> if Eq/Hash should agree with str
impl Borrow<str> for Name { fn borrow(&self) -> &str { &self.0 } }
多相性のためにDerefを使うのは避けます:Derefはスマートポインター(Box、Rc、Arc、Stringなど。標準ライブラリの先例を参照)向けです。Derefを使ってMyClientをHttpClientに透過させると、メソッドが暗黙に追加され、&selfの解決が壊れ、deref多相性のアンチパターンになります。AsRef、明示的なメソッド、またはコンポジションを使います。
C-ITER — イテレーターの設計
// ✅ Provide IntoIterator for &T, &mut T, T when sensible
impl<'a> IntoIterator for &'a Grid {
type Item = &'a Cell;
type IntoIter = std::slice::Iter<'a, Cell>;
fn into_iter(self) -> Self::IntoIter { self.cells.iter() }
}
impl Grid {
pub fn iter(&self) -> impl Iterator<Item = &Cell> { /* */ }
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Cell> { /* */ }
}
impl Extend<Cell> for Grid { /* */ }
ルール:iter()は借用し、iter_mut()は可変借用し、into_iter()は消費します。イテレーション用メソッドからVecを返してはいけません。impl Iteratorを返します。
C-SERDE — シリアライゼーションの相互運用
- 型内では一貫して
#[serde(rename_all = "kebab-case")]を使います #[non_exhaustive]の列挙型では、未知のバリアントとの前方互換性のために#[serde(other)]が必要です- 下流のクレートがオプトアウトできるよう、機能(
serde = ["dep:serde"])を通じてserdeを再エクスポートします
3. 予測可能性(C-INTUITIVE、C-CONST、C-COMMON)
C-INTUITIVE — 名前でセマンティクスを表す
呼び出し側がreadの動作を知るためにソースを読む必要があるなら、そのAPIは適切ではありません。予想外の動作は避けます。EOFでパニックするreadメソッドは欠陥です。
C-CONST — 可能な場合はconst fnにする
pub const fn new(value: u32) -> Self { Self(value) }
const fn is_power_of_two(n: u32) -> bool { n != 0 && n & (n - 1) == 0 }
const fnにできる標準ライブラリのAPIはすべて候補になります。これにより、呼び出し側でconst MAX: UserId = UserId::new(1000);を使えるようになります。
C-COMMON — Defaultとビルダーで適切なデフォルト値を提供する
impl Default for Config {
fn default() -> Self {
Config { retry_count: 3, timeout: Duration::from_secs(30) }
}
}
// Builder for complex construction
let client = Client::builder().with_retry(3).with_timeout(Duration::from_secs(10)).build()?;
4. 柔軟性(C-OVERLOAD、C-GENERIC、C-NEWTYPE、C-EXT)
C-GENERIC — 入力はジェネリックにし、出力は具体型にする
// ✅ Generic over AsRef<str> — caller passes &str, String, Cow
pub fn parse(input: impl AsRef<str>) -> Result<Foo> { /* */ }
// ❌ Concrete &str — forces caller to borrow
pub fn parse(input: &str) -> Result<Foo> { /* */ }
トレードオフ:ジェネリックが増えるほど、コンパイル時間が長くなり、診断が難しくなります。入力型(AsRef<str>、IntoIteratorなど)はジェネリックにし、出力型は具体型にします。
C-NEWTYPE — プリミティブ型をラップして誤用を防ぐ
// ✅ Newtypes around raw primitives
pub struct UserId(pub u64);
pub struct Email(String); // private inner — can't be constructed unsafely
fn delete_user(id: UserId) { /* */ } // can't accidentally pass a PostId
// ❌ Plain primitives — confusion and argument-order bugs
fn delete_user(id: u64) { /* */ }
ゼロコスト(基になる型にコンパイルされます)で、引数の順序の誤りを防ぎ、メソッド(UserId::is_anonymous()など)を追加できます。
C-EXT — 拡張トレイトにはExtサフィックスを付ける
pub trait StringExt { fn slugify(&self) -> String; }
impl StringExt for str { fn slugify(&self) -> String { /* */ } }
// Caller opts in:
use my_crate::StringExt;
"Hello World".slugify();
別のクレートからString/Vec/HttpRequestに直接メソッドを追加してはいけません。Extトレイトを使います。
5. 型安全性(C-BOOL、C-NONZERO、C-STR、C-SIGNED、C-BITFLAG、C-WRAPPER、C-INTERVAL)
権威ある資料:API Guidelines — Type Safety。標準的な表現については、
references/api-guidelines-checklist.mdを参照してください。
C-BOOL — boolパラメーターを列挙型に置き換える
// ✅ Enum — caller intent is explicit at call site
pub enum Trim { Whitespace, None }
pub fn parse(input: &str, trim: Trim) -> Result<Foo> { /* */ }
parse(" x ", Trim::Whitespace);
// ❌ Bool — caller must remember what true means
pub fn parse(input: &str, trim: bool) -> Result<Foo> { /* */ }
parse(" x ", true); // true = ??
boolパラメーターが2つあると複雑になります。f(true, false, true)では意味が分かりません。引数を取る列挙型を最低限使います。機械的に強制するには、clippy.tomlでmax-fn-params-bools = 1とmax-struct-bools = 1を設定します。
C-NONZERO — ゼロが無効な場合はNonZeroUsizeを使う
use std::num::NonZeroUsize;
// ✅ NonZeroUsize encodes "≥ 1" in the type
pub fn chunk_size(&self) -> NonZeroUsize { /* */ }
ニッチ最適化が有効になります。Option<NonZeroU32>はu32と同じサイズです。NonZeroU8/NonZeroU16/NonZeroU32/NonZeroU64/NonZeroUsizeと、NonZeroI*の各バリアントを使います。
C-STR — &Stringではなく&strを、&Vec<T>ではなく&[T]を使う
// ✅ Borrow slices for inputs
pub fn process(data: &[u8], name: &str) { /* */ }
// ❌ Forces caller to have owned collections
pub fn process(data: &Vec<u8>, name: &String) { /* */ }
C-SIGNED — 値が負にならない場合は符号なし型を優先する
// ✅ u64 — semantically "count" can't be negative
pub struct Counter { count: u64 }
// ❌ i64 — implies negative values are valid (they aren't)
pub struct Counter { count: i64 }
特別な範囲(例:Ageの0..=150)の場合は、検証するコンストラクターを備えたニュータイプを使い、型システムによって無効な値を防ぎます。
C-BITFLAG — フラグの集合にはbitflags!マクロを使う
use bitflags::bitflags;
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Permissions: u32 {
const READ = 0b001;
const WRITE = 0b010;
const EXECUTE = 0b100;
}
}
let p = Permissions::READ | Permissions::WRITE; // type-checked composition
assert!(p.contains(Permissions::READ)); // built-in methods
フラグの集合に生のu32を使うのは避けます。型安全性が失われ、contains/insert/remove/intersectsも使えなくなります。
C-WRAPPER — プリミティブ型に意味のあるセマンティクスを持たせるにはニュータイプを使う
pub struct UserId(pub u64);
pub struct AccountId(pub u64);
pub struct OrderId(pub u64);
// Compiler rejects wrong-id bugs:
fn transfer(from: AccountId, to: AccountId, amount: Cents) { /* */ }
// transfer(UserId(1), UserId(2), Cents(100)) ← compile error
実行時にはゼロコスト(基になる型にコンパイルされます)。デフォルトでは#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]を使います。
C-INTERVAL — 範囲は緩いペアではなく型として表現する
// ✅ Dedicated range type with validating constructor
pub struct ChunkRange { start: u32, end: u32 } // invariant: end >= start
impl ChunkRange {
pub fn new(start: u32, end: u32) -> Result<Self, RangeError> {
if end < start { return Err(RangeError::Inverted); }
Ok(Self { start, end })
}
pub fn contains(&self, x: u32) -> bool { self.start <= x && x <= self.end }
}
// ❌ Loose pair — caller might pass end < start
pub fn process_chunk(start: u32, end: u32) { /* */ }
標準の範囲には、RangeInclusive/Range/RangeTo を使用します。ページネーションや時間ウィンドウなど、ドメイン固有の範囲は新しい型でラップします。
C-COMMENT-HIDDEN — #[doc(hidden)] は公開 API から除外しません
// ❌ Hides from rustdoc but is still semver-relevant
#[doc(hidden)]
pub mod unstable { /* */ } // downstream can still `use crate::unstable::Foo;`
// ✅ For actually-unstable items, gate behind a feature
#[cfg(feature = "unstable")]
pub mod unstable { /* */ }
#[doc(hidden)] は cargo doc で非表示にするだけです。semver や安定性を管理するには、フィーチャーフラグまたはモジュールのプライバシーを使用します。
所有権を持つ入力には、String/Vec または impl Into<String>/impl IntoIterator を受け取ります。
6. 依存性(C-PANIC、C-UNWRAP、C-TRANSMUTE)
C-PANIC — 到達不能な不変条件に限りパニックを許容する
// ✅ Documented, unreachable from public API
pub fn lookup(&self, id: UserId) -> &User {
self.table.get(&id).expect("internal: index invariant broken")
}
// ❌ Panics on user input
pub fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 { panic!("division by zero") }
a / b
}
// ✅ Return Result for fallible operations
pub fn divide(a: f64, b: f64) -> Result<f64, DivError> { /* */ }
許容されるケース:プログラマーが不変条件に違反した場合、または関数が失敗しないことを文書化している場合です。許容されないケース:入力に起因する失敗です。
C-UNWRAP — 公開パスでの unwrap/expect を禁止する
// ❌ Library code
pub fn parse(input: &str) -> Config {
serde_json::from_str(input).unwrap() // panics on bad input
}
// ✅ Propagate
pub fn parse(input: &str) -> Result<Config, ParseError> {
Ok(serde_json::from_str(input)?)
}
unwrap はテスト、const コンテキスト(? を使えない場合)、および到達不能であることが証明されたコードでは使用できます。診断情報のため、裸の unwrap() より expect("reason") を使用します。
C-TRANSMUTE — 型の再解釈に std::mem::transmute を使わない
transmute は型システムを迂回してビットを再解釈します。代わりに From/Into/TryFrom、bytemuck::cast/zerocopy(検証済みのバイトキャスト)、u64::from_ne_bytes/to_ne_bytes、または拡張変換には as を使用します。transmute を使おうとしている場合は、rust-unsafe-ffi に切り替えてください。
7. デバッグのしやすさ(C-DEBUG)
すべての公開型に Debug を実装します。Debug がないとデバッグの妨げになり、Debug で情報を過剰に公開すると秘密情報が漏えいします。
#[derive(Debug)]
pub struct Client { /* */ }
// ✅ Manual Debug for secrets
impl std::fmt::Debug for Password {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Password(***)")
}
}
8. 将来への備え(C-SEALED、C-STRUCT-FIELD、C-NON-EXHAUSTIVE)
C-SEALED — トレイトをシールして外部からの実装を防ぐ
mod private { pub trait Sealed {} }
pub trait Serializer: private::Sealed {
fn serialize(&self, v: &impl Serialize);
}
pub struct JsonSerializer;
impl private::Sealed for JsonSerializer {}
impl Serializer for JsonSerializer { /* */ }
シールしないと、誰でも impl MyTrait for TheirType を記述できるため、メソッドを追加できなくなります(追加すると破壊的変更になります)。references/future-proofing.md を参照してください。
C-STRUCT-FIELD — 公開構造体フィールドは永久に安定した API となる
// ✅ Private fields, public constructor — adding fields is non-breaking
pub struct Config { retry_count: u32, timeout: Duration }
impl Config {
pub fn new(retry: u32, timeout: Duration) -> Self { Self { retry_count: retry, timeout } }
pub fn builder() -> ConfigBuilder { /* */ }
}
// ❌ Pub fields + struct literal — adding a field breaks callers
let c = Config { retry_count: 3, timeout: Duration::from_secs(30) };
ルール:pub フィールド=永久に安定。プライベートフィールド=変更可能。
C-NON-EXHAUSTIVE — 拡張予定の列挙型に指定する
#[non_exhaustive]
pub enum Error { Io(io::Error), Parse(ParseError) }
// Downstream must use a wildcard arm:
match err {
Error::Io(e) => /* */,
Error::Parse(e) => /* */,
_ => /* unknown */,
}
拡張する予定のエラー/イベント/ステータス列挙型と、フィールドを追加する可能性がある公開フィールド付き構造体に使用します。
ワークフロー
- 公開 API を洗い出す —
cargo doc --openを実行し、すべてのpub項目を列挙します。これが契約です。 - 命名規則を適用する(セクション 1)— 大文字小文字、変換プレフィックス(
as_/to_/into_)を修正し、get_を削除します。 - 相互運用性の規則を適用する(セクション 2)—
Debug/Cloneなどを導出し、From/AsRefを追加して、イテレーターを設計します。 - 予測可能性と柔軟性を適用する(セクション 3~4)—
Default、const fn、プリミティブ型の新しい型、Extトレイトを使用します。 - 型安全性を適用する(セクション 5)— bool を列挙型に置き換え、
NonZero*を使用し、&Stringより&strを選びます。 - 依存性の規則を適用する(セクション 6)— 公開パスから
unwrap/panic!を削除し、Resultを返します。 - 将来への備えを適用する(セクション 8)— 拡張可能なトレイトをシールし、拡張予定の列挙型に
#[non_exhaustive]を指定し、構造体フィールドをプライベートにします。 - 引き継ぐ — semver のチェック →
rust-semver、ドキュメントテスト →rust-documentation、lint 設定 →rust-style-clippy。
判断の早見表
| 質問 | 回答 |
|---|---|
Option と Result のどちらを使うべきですか? | 「存在しない場合がある」なら Option、「失敗する可能性がある」なら Result |
| トレイトと列挙型のどちらを使うべきですか? | オープン(他の人が実装を追加する)ならトレイト、クローズド(バリアントを自分で管理する)なら列挙型 |
| ジェネリック型と具体型のどちらを使うべきですか? | 入力にはジェネリック型、出力には具体型 |
フィールドを pub にすべきですか? | 既定ではいいえ。構造体リテラルでの構築を意図している場合に限ります |
&str と impl AsRef<str> のどちらを使うべきですか? | 柔軟性を求めるなら AsRef<str>、シンプルさを求めるなら &str |
Copy を導出すべきですか? | 小さい(≤16 バイト)かつビット単位のコピーが正しい場合に限ります |
| このトレイトをシールすべきですか? | サードパーティーによる実装の追加が必要な場合(まれです)を除き、はい |
#[non_exhaustive] を指定すべきですか? | エラー/イベント列挙型にははい。拡張する可能性のある公開フィールド付き構造体にもはい |
アンチパターンカタログ
Derefによるポリモーフィズム — メソッドを「継承」するためにDerefを使います。コンポジションを使ってください。get_Xアクセサー —get_を削除します。fn data(&self) -> &[u8]。- bool パラメーター —
fn f(x: bool, y: bool)。2 つの列挙型を使ってください。 - 公開 API での
unwrap— パニックが伝播します。Resultを使ってください。 - 入力での
String/Vec— 所有権を強制します。&str/&[T]を使ってください。 - 封印されていない拡張トレイト — 将来メソッドを追加できなくなります。封印してください。
- エラーに
#[non_exhaustive]がない — バリアントの追加で下流のコードが壊れます。 - Newtype がない —
fn transfer(amount: u64, from: u64, to: u64)— 引数の順序を間違えやすくなります。 - 生成された
Debugによる秘密情報の漏えい —#[derive(Debug)] struct ApiKey(String)。独自の実装で値をマスキングしてください。 - キャストに
transmuteを使う —From/as/from_ne_bytesを使ってください。
それぞれのリファクタリング前後の例については、examples/anti-patterns.md を参照してください。
リソース
- API ガイドラインのチェックリスト — 例を含む C-* ルール約 100 件の全文
- 命名と変換の詳細解説
- 将来の変更に備えるパターン — 封印されたトレイト、non_exhaustive、ビルダーパターン
- アンチパターンカタログ — リファクタリング前後の例
examples/golden-api/— すべてのルールに従い、cargo docと clippy が通る小さなクレート