Api Design logo

Api Design

Community
wshaddix
api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, versioning, naming conventions, parameter ordering, and return types for NuGet packages and distributed systems. Use when designing public APIs for NuGet packages or libraries, making changes to existing public APIs, planning wire format changes for distributed systems, or reviewing pull requests for breaking changes.

Overview

Publisherwshaddix
Repositorydotnet-skills
Skill nameapi-design
Stars
79
Forks
13
Bundled files
Instructions only
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by wshaddix on GitHub. Read the source before you install it.

Installation

Install the Api Design AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/wshaddix/dotnet-skills.git /tmp/dotnet-skills
mkdir -p .claude/skills
cp -r /tmp/dotnet-skills/skills/csharp-api-design .claude/skills/api-design
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Api Design in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Api Design on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Api Design is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

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

TypeDefinitionScope
API/SourceCode compiles against newer versionPublic method signatures, types
BinaryCompiled code runs against newer versionAssembly layout, method tokens
WireSerialized data readable by other versionsNetwork 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

Naming Conventions for API Surface

Type Naming

Type KindSuffix PatternExample
Base classBase suffix only for abstract base typesValidatorBase
InterfaceI prefixIWidgetFactory
ExceptionException suffixWidgetNotFoundException
AttributeAttribute suffixRequiredPermissionAttribute
Event argsEventArgs suffixWidgetCreatedEventArgs
Options/configOptions suffixWidgetServiceOptions
BuilderBuilder suffixWidgetBuilder

Method Naming

PatternConventionExample
SynchronousVerb or verb phraseCalculate(), GetWidget()
AsynchronousAsync suffixCalculateAsync(), GetWidgetAsync()
Boolean queryIs/Has/Can prefixIsValid(), HasPermission()
Try patternTry prefix, out parameterTryGetWidget(int id, out Widget widget)
FactoryCreate prefixCreateWidget(), CreateWidgetAsync()
ConversionTo/From prefixToDto(), FromEntity()

Avoid Abbreviations in Public API

csharp
// WRONG -- abbreviations in public surface
public IReadOnlyList<TxnResult> GetRecentTxns(int cnt);

// CORRECT -- spelled out for clarity
public IReadOnlyList<TransactionResult> GetRecentTransactions(int count);

Parameter Ordering

Consistent parameter ordering reduces cognitive load.

Standard Order

  1. Target/subject -- the primary entity being operated on
  2. Required parameters -- essential inputs without defaults
  3. Optional parameters -- inputs with sensible defaults
  4. Cancellation token -- always last (convention enforced by CA1068)
csharp
public Task<Widget> GetWidgetAsync(
    int widgetId,                              // 1. Target
    WidgetOptions options,                     // 2. Required
    bool includeHistory = false,               // 3. Optional
    CancellationToken cancellationToken = default); // 4. Always last

Overload Progression

csharp
// Simple -- sensible defaults
public Task<Widget> GetWidgetAsync(int widgetId,
    CancellationToken cancellationToken = default)
    => GetWidgetAsync(widgetId, WidgetOptions.Default, cancellationToken);

// Detailed -- full control
public Task<Widget> GetWidgetAsync(int widgetId,
    WidgetOptions options,
    CancellationToken cancellationToken = default);

Return Type Selection

When to Return What

ScenarioReturn TypeRationale
Single entity, always existsWidgetThrow if not found
Single entity, may not existWidget?Nullable communicates optionality
Collection, possibly emptyIReadOnlyList<Widget>Immutable, indexable, communicates no mutation
Streaming resultsIAsyncEnumerable<Widget>Avoids buffering entire result set
Operation result with detailResult<Widget> / discriminated unionRich error info without exceptions
Void with asyncTaskNever async void except event handlers
Frequently synchronous completionValueTask<Widget>Avoids Task allocation on cache hits

Prefer IReadOnlyList Over IEnumerable

csharp
// WRONG -- caller does not know if result is materialized or lazy
public IEnumerable<Widget> GetWidgets();

// CORRECT -- signals materialized, indexable collection
public IReadOnlyList<Widget> GetWidgets();

// CORRECT -- signals streaming/lazy evaluation explicitly
public IAsyncEnumerable<Widget> GetWidgetsStreamAsync(
    CancellationToken cancellationToken = default);

The Try Pattern

csharp
public bool TryGetWidget(int widgetId, [NotNullWhen(true)] out Widget? widget);

public Task<Widget?> TryGetWidgetAsync(int widgetId,
    CancellationToken cancellationToken = default);

Error Reporting Strategies

Exception Hierarchy

csharp
public class WidgetServiceException : Exception
{
    public WidgetServiceException(string message) : base(message) { }
    public WidgetServiceException(string message, Exception inner) : base(message, inner) { }
}

public class WidgetNotFoundException : WidgetServiceException
{
    public int WidgetId { get; }
    public WidgetNotFoundException(int widgetId)
        : base($"Widget {widgetId} not found.") => WidgetId = widgetId;
}

public class WidgetValidationException : WidgetServiceException
{
    public IReadOnlyList<string> Errors { get; }
    public WidgetValidationException(IReadOnlyList<string> errors)
        : base("Widget validation failed.") => Errors = errors;
}

When to Use Exceptions vs Return Values

ApproachWhen to Use
Throw exceptionUnexpected failures, programming errors, infrastructure failures
Return null / default"Not found" is a normal, expected outcome
Try pattern (bool + out)Parsing or validation where failure is common and synchronous
Result objectMultiple failure modes that callers need to distinguish

Argument Validation

csharp
public Widget CreateWidget(string name, decimal price)
{
    ArgumentException.ThrowIfNullOrWhiteSpace(name);
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(price);

    return new Widget(name, price);
}

API Change Guidelines

Safe Changes (Any Release)

csharp
// ADD new overloads with default parameters
public void Process(Order order, CancellationToken ct = default);

// ADD new optional parameters to existing methods
public void Send(Message msg, Priority priority = Priority.Normal);

// 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)

csharp
// 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 required parameters without defaults
public void Process(Order order, ILogger logger);  // Breaks callers!

Deprecation Pattern

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

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

// Step 3: Remove in next major version

Extension Points

Interface-Based Extension

csharp
// GOOD -- interface-based extension point
public interface IWidgetValidator
{
    ValueTask<bool> ValidateAsync(Widget widget, CancellationToken ct = default);
}

// GOOD -- delegate-based extension for simple hooks
public class WidgetServiceOptions
{
    public Func<Widget, CancellationToken, ValueTask>? OnWidgetCreated { get; set; }
}

Extension Method Guidelines

GuidelineRationale
Place extensions in the same namespace as the typeDiscoverable without extra using statements
Never put extensions in System or System.LinqNamespace pollution
Prefer instance methods over extensions when you own the typeExtensions are a last resort
Keep the this parameter as the most specific usable typeAvoids polluting IntelliSense

Wire Compatibility

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

Requirements

DirectionRequirement
BackwardOld writers → New readers
ForwardNew writers → Old readers

Both are required for zero-downtime rolling upgrades.

Safely Evolving Wire Formats

Phase 1: Add read-side support

csharp
public sealed record HeartbeatV2(
    Address From,
    long SequenceNr,
    long CreationTimeMs);  // NEW field

public object Deserialize(byte[] data, string manifest) => manifest switch
{
    "Heartbeat" => DeserializeHeartbeatV1(data),
    "HeartbeatV2" => DeserializeHeartbeatV2(data),
    _ => throw new NotSupportedException()
};

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

csharp
akka.cluster.use-heartbeat-v2 = on

Defensive Serialization Design

csharp
public sealed class WidgetDto
{
    [JsonPropertyName("id")]
    public int Id { get; init; }

    [JsonPropertyName("name")]
    public required string Name { get; init; }

    [JsonPropertyName("category")]
    public string? Category { get; init; }

    [JsonPropertyName("priority")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public int Priority { get; init; }
}

Enum Serialization Strategy

csharp
// GOOD -- string serialization is rename-safe
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum WidgetStatus
{
    Draft,
    Active,
    Archived
}

// RISKY -- integer serialization breaks when members are reordered
public enum WidgetPriority
{
    Low = 0,
    Medium = 1,
    High = 2
}

API Approval Testing

Prevent accidental breaking changes with automated API surface testing.

csharp
[Fact]
public Task ApprovePublicApi()
{
    var api = typeof(MyLibrary.PublicClass).Assembly.GeneratePublicApi();
    return Verify(api);
}

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

Versioning Strategy

Semantic Versioning (Practical)

VersionChanges 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
  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

Pull Request Checklist

  • 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

csharp
// "Bug fix" that breaks users
public async Task<Order> GetOrderAsync(OrderId id)  // Was sync!
{
}

// 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

csharp
// Changing defaults breaks users
public void Configure(bool enableCaching = true)  // Was: false!

// Correct: New parameter with new name
public void Configure(
    bool enableCaching = false,
    bool enableNewCaching = true)

Polymorphic Serialization

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

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

Agent Gotchas

  1. Do not use abbreviations in public API names -- spell out words.
  2. Do not place CancellationToken before optional parameters -- CA1068 enforces last.
  3. Do not return mutable collections from public APIs -- return IReadOnlyList<T>.
  4. Do not change serialized property names without [JsonPropertyName] annotations.
  5. Do not add required parameters to existing public methods -- add overload or use defaults.
  6. Do not use async void in API surface -- return Task or ValueTask.
  7. Do not design exception hierarchies without a base library exception.
  8. Do not put extension methods in the System namespace.

Resources

Frequently asked questions

What does the Api Design AI skill do?

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, versioning, naming conventions, parameter ordering, and return types for NuGet packages and distributed systems. Use when designing public APIs for NuGet packages or libraries, making changes to existing public APIs, planning wire format changes for distributed systems, or reviewing pull requests for breaking changes.

Why use Api Design on TypingMind?

Because you install it once and use it with any model. Api Design is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Api Design in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/wshaddix/dotnet-skills/tree/master/skills/csharp-api-design. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Api Design?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Api Design?

As many as you like. As long as a model supports skills, you can use Api Design with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Api Design AI skill free?

It is published on GitHub by wshaddix. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇