Skip to content
alyn
Aaronontheweb

API Design skill

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

$npx skills add Aaronontheweb/dotnet-skills --skill api-design
View this skill on GitHubAaronontheweb/dotnet-skills/skills/csharp-api-design/SKILL.md
Pack stars
1,257
Pack installs
23k
Alyn Score
61/99
Updated
13 days ago
License
MIT
– views – installs via Alyn

Aaronontheweb

API Design

Install API Design

Pick your agent. Every command is ready to paste.

$npx skills add Aaronontheweb/dotnet-skills --skill api-design

The open skills CLI detects your agents (Claude Code, Cursor, Codex and 70+ more) and installs to the right folder. Add -g to install globally.

01

Install it

Run the command above, or upload the folder in Claude.

02

Ask naturally

Describe the task. Your agent loads api-design when the request matches its description.

03

Name it if needed

If it does not trigger, say “use the api-design skill”. In Codex, type $api-design.

The skill itself

SKILL.md

FolderGitHub

Public API Design and Compatibility

When to Use This Skill

Use this skill when:

  • Designing public APIs for NuGet packages or libraries
  • Making changes to existing public APIs
  • Planning wire format changes for distributed systems
  • Implementing versioning strategies
  • Reviewing pull requests for breaking changes

The Three Types of Compatibility

Type Definition Scope
API/Source Code compiles against newer version Public method signatures, types
Binary Compiled code runs against newer version Assembly layout, method tokens
Wire Serialized data readable by other versions Network protocols, persistence formats

Breaking any of these creates upgrade friction for users.


Extend-Only Design

The foundation of stable APIs: never remove or modify, only extend.

Three Pillars

  1. Previous functionality is immutable - Once released, behavior and signatures are locked
  2. New functionality through new constructs - Add overloads, new types, opt-in features
  3. Removal only after deprecation period - Years, not releases

Benefits

  • Old code continues working in new versions
  • New and old pathways coexist
  • Upgrades are non-breaking by default
  • Users upgrade on their schedule

Resources:


API Change Guidelines

Safe Changes (Any Release)

// SAFE: Add NEW overload methods that delegate to existing methods
// Existing method - do not modify its signature
public void Process(Order order) { ... }
// New overload - safe to add
public void Process(Order order, CancellationToken ct)
{
    // implementation that handles cancellation
}

// SAFE: Add NEW overloads for additional functionality
// Existing method - do not modify
public void Send(Message msg) { ... }
// New overload - safe to add
public void Send(Message msg, Priority priority)
{
    // implementation that handles priority
}

// ADD new types, interfaces, enums
public interface IOrderValidator { }
public enum OrderStatus { Pending, Complete, Cancelled }

// ADD new members to existing types
public class Order
{
    public DateTimeOffset? ShippedAt { get; init; }  // NEW
}

Unsafe Changes (Never or Major Version Only)

// REMOVE or RENAME public members
public void ProcessOrder(Order order);  // Was: Process()

// CHANGE parameter types or order
public void Process(int orderId);  // Was: Process(Order order)

// CHANGE return types
public Order? GetOrder(string id);  // Was: public Order GetOrder()

// CHANGE access modifiers
internal class OrderProcessor { }  // Was: public

// ADD optional parameters to EXISTING methods (binary incompatible!)
// The compiled IL method signature changes - callers compiled against
// the old signature will get MissingMethodException at runtime.
// Optional parameter defaults are baked into the CALLER's assembly at compile time.
public void Process(Order order, CancellationToken ct = default);  // Breaks binary compat!
public void Send(Message msg, Priority priority = Priority.Normal);  // Breaks binary compat!
// Correct approach: add a NEW overload method instead (see Safe Changes above)

// ADD required parameters without defaults
public void Process(Order order, ILogger logger);  // Breaks callers!

Deprecation Pattern

// Step 1: Mark as obsolete with version (any release)
[Obsolete("Obsolete since v1.5.0. Use ProcessAsync instead.")]
public void Process(Order order) { }

// Step 2: Add new recommended API (same release)
public Task ProcessAsync(Order order, CancellationToken ct = default);

// Step 3: Remove in next major version (v2.0+)
// Only after users have had time to migrate

API Approval Testing

Prevent accidental breaking changes with automated API surface testing.

Using ApiApprover + Verify

dotnet add package PublicApiGenerator
dotnet add package Verify.Xunit
[Fact]
public Task ApprovePublicApi()
{
    var api = typeof(MyLibrary.PublicClass).Assembly.GeneratePublicApi();
    return Verify(api);
}

Creates ApprovePublicApi.verified.txt:

namespace MyLibrary
{
    public class OrderProcessor
    {
        public OrderProcessor() { }
        public void Process(Order order) { }
        public Task ProcessAsync(Order order, CancellationToken ct = default) { }
    }
}

Any API change fails the test - reviewer must explicitly approve changes.

PR Review Process

  1. PR includes changes to *.verified.txt files
  2. Reviewers see exact API surface changes in diff
  3. Breaking changes are immediately visible
  4. Conscious decision required to approve

Wire Compatibility

For distributed systems, serialized data must be readable across versions.

Requirements

Direction Requirement
Backward Old writers → New readers (current version reads old data)
Forward New writers → Old readers (old version reads new data)

Both are required for zero-downtime rolling upgrades.

Safely Evolving Wire Formats

Phase 1: Add read-side support (opt-in)

// New message type - readers deployed first
public sealed record HeartbeatV2(
    Address From,
    long SequenceNr,
    long CreationTimeMs);  // NEW field

// Deserializer handles both old and new
public object Deserialize(byte[] data, string manifest) => manifest switch
{
    "Heartbeat" => DeserializeHeartbeatV1(data),   // Old format
    "HeartbeatV2" => DeserializeHeartbeatV2(data), // New format
    _ => throw new NotSupportedException()
};

Phase 2: Enable write-side (opt-out, next minor version)

// Config to enable new format (off by default initially)
akka.cluster.use-heartbeat-v2 = on

Phase 3: Make default (future version)

After install base has absorbed read-side code.

Schema-Based Serialization

Prefer schema-based formats over reflection-based:

Format Type Wire Compatibility
Protocol Buffers Schema-based Excellent - explicit field numbers
MessagePack Schema-based Good - with contracts
System.Text.Json Schema-based (with source gen) Good - explicit properties
Newtonsoft.Json Reflection-based Poor - type names in payload
BinaryFormatter Reflection-based Terrible - never use

See dotnet/serialization skill for details.


Encapsulation Patterns

Internal APIs

Mark non-public APIs explicitly:

// Attribute for documentation
[InternalApi]
public class ActorSystemImpl { }

// Namespace convention
namespace MyLibrary.Internal
{
    public class InternalHelper { }  // Public for extensibility, not for users
}

Document clearly:

Types in .Internal namespaces or marked with [InternalApi] may change between any releases without notice.

Sealing Classes

// DO: Seal classes not designed for inheritance
public sealed class OrderProcessor { }

// DON'T: Leave unsealed by accident
public class OrderProcessor { }  // Users might inherit, blocking changes

Interface Segregation

// DO: Small, focused interfaces
public interface IOrderReader
{
    Order? GetById(OrderId id);
}

public interface IOrderWriter
{
    Task SaveAsync(Order order);
}

// DON'T: Monolithic interfaces (can't add methods without breaking)
public interface IOrderRepository
{
    Order? GetById(OrderId id);
    Task SaveAsync(Order order);
    // Adding new methods breaks all implementations!
}

Versioning Strategy

Semantic Versioning (Practical)

Version Changes Allowed
Patch (1.0.x) Bug fixes, security patches
Minor (1.x.0) New features, deprecations, obsolete removal
Major (x.0.0) Breaking changes, old API removal

Key Principles

  1. No surprise breaks - Even major versions should be announced and planned
  2. Extensions anytime - New APIs can ship in any release
  3. Deprecate before remove - [Obsolete] for at least one minor version
  4. Communicate timelines - Users need to plan upgrades

Chesterton's Fence

Before removing or changing something, understand why it exists.

Assume every public API is used by someone. If you want to change it:

  1. Socialize the proposal on GitHub
  2. Document migration path
  3. Provide deprecation period
  4. Ship in planned release

Pull Request Checklist

When reviewing PRs that touch public APIs:

  • No removed public members (use [Obsolete] instead)
  • No changed signatures (add overloads instead)
  • No new required parameters (use defaults)
  • API approval test updated (.verified.txt changes reviewed)
  • Wire format changes are opt-in (read-side first)
  • Breaking changes documented (release notes, migration guide)

Anti-Patterns

Breaking Changes Disguised as Fixes

// "Bug fix" that breaks users
public async Task<Order> GetOrderAsync(OrderId id)  // Was sync!
{
    // "Fixed" to be async - but breaks all callers
}

// Correct: Add new method, deprecate old
[Obsolete("Use GetOrderAsync instead")]
public Order GetOrder(OrderId id) => GetOrderAsync(id).Result;

public async Task<Order> GetOrderAsync(OrderId id) { }

Silent Behavior Changes

// Changing defaults breaks users who relied on old behavior
public void Configure(bool enableCaching = true)  // Was: false!

// Correct: New parameter with new name
public void Configure(
    bool enableCaching = false,  // Original default preserved
    bool enableNewCaching = true)  // New behavior opt-in

Polymorphic Serialization

// AVOID: Type names in wire format
{ "$type": "MyApp.Order, MyApp", "Id": 123 }

// Renaming Order class = wire break!

// PREFER: Explicit discriminators
{ "type": "order", "id": 123 }

Resources

Tips for API Design

  • • Start a fresh session after installing so your agent picks up the new skill.
  • • It is part of a pack with 30 skills. Install only the ones you need with --skill to keep triggers precise.
  • • If two skills overlap, mention the one you want by name.

Questions about API Design

What does the API Design skill do?

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

How do I install API Design?

Run "npx skills add Aaronontheweb/dotnet-skills --skill api-design" in your project. The skills CLI installs it for Claude Code, Cursor, Codex and 70+ other agents. In Claude on the web, upload the skill folder as a ZIP under Customize > Skills.

Does API Design work in Cursor and Codex?

Yes. API Design uses the open SKILL.md format, so it works in Claude Code, Claude, Cursor, OpenAI Codex, Gemini CLI, GitHub Copilot and other agents that support agent skills.

Is API Design free?

API Design is open source on GitHub under the MIT license. It is free to install; check the license before commercial use.

Who made API Design?

API Design is published by Aaronontheweb in the Aaronontheweb/dotnet-skills repository, together with 29 other skills.

Similar skills. From other creators.