Claude-skill-registry fsharp-shared
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fsharp-shared" ~/.claude/skills/majiayu000-claude-skill-registry-fsharp-shared && rm -rf "$T"
manifest:
skills/data/fsharp-shared/SKILL.mdsource content
F# Shared Types and API Contracts
When to Use This Skill
Activate when:
- Starting any new feature (always define types first)
- User requests "add X entity", "define Y types"
- Need to create API contracts between client and server
- Modifying existing domain types
- Creating shared data structures
Prerequisites
Project must have:
for domain typessrc/Shared/Domain.fs
for API contractssrc/Shared/Api.fs- Fable.Remoting package installed
Type Design Patterns
Simple Entity (Records)
Use for: Basic data structures with named fields
// src/Shared/Domain.fs module Shared.Domain open System type TodoItem = { Id: int Title: string Description: string option IsCompleted: bool CreatedAt: DateTime UpdatedAt: DateTime }
Key points:
- Use records (not classes)
- Use
for nullable fieldsoption - Include timestamps for auditing
- Immutable by default
Discriminated Unions
Use for: Fixed sets of values or state machines
type Priority = | Low | Medium | High | Urgent type TodoStatus = | NotStarted | InProgress | Completed | Cancelled type TodoItem = { Id: int Title: string Priority: Priority Status: TodoStatus CreatedAt: DateTime }
Key points:
- Exhaustive pattern matching
- Compiler-enforced state transitions
- Self-documenting code
Smart Constructors (Constrained Types)
Use for: Types with validation rules
type EmailAddress = private EmailAddress of string module EmailAddress = let create (s: string) : Result<EmailAddress, string> = if s.Contains("@") && s.Length > 3 then Ok (EmailAddress s) else Error "Invalid email format" let value (EmailAddress s) = s type User = { Id: int Name: string Email: EmailAddress // Guaranteed valid }
Key points:
- Private constructor prevents invalid instances
- Factory function enforces validation
- Type system ensures correctness
Collections and Nested Types
type TodoList = { Id: int Name: string Items: TodoItem list Owner: User CreatedAt: DateTime } type Dashboard = { User: User Lists: TodoList list TotalItems: int }
API Contract Patterns
Basic CRUD API
Location:
src/Shared/Api.fs
module Shared.Api open Domain type ITodoApi = { // Queries (always succeed, return empty on no data) getAll: unit -> Async<TodoItem list> // Queries that may fail (use Result) getById: int -> Async<Result<TodoItem, string>> // Commands that may fail create: TodoItem -> Async<Result<TodoItem, string>> update: TodoItem -> Async<Result<TodoItem, string>> delete: int -> Async<Result<unit, string>> }
Return type guide:
- Always returns (empty list if none)Async<'T list>
- May fail (not found, validation error)Async<Result<'T, string>>
- Success with no data to returnAsync<Result<unit, string>>
API with DTOs (Create/Update Models)
Use when: Create and update have different fields
type CreateTodoRequest = { Title: string Description: string option Priority: Priority } type UpdateTodoRequest = { Id: int Title: string Description: string option Priority: Priority Status: TodoStatus } type ITodoApi = { getAll: unit -> Async<TodoItem list> getById: int -> Async<Result<TodoItem, string>> create: CreateTodoRequest -> Async<Result<TodoItem, string>> update: UpdateTodoRequest -> Async<Result<TodoItem, string>> delete: int -> Async<Result<unit, string>> }
Key points:
- Separate request models from domain entities
- Client doesn't set server-managed fields (Id, timestamps)
- Clearer intent (create vs update)
Multiple API Interfaces
Use when: Logically separate concerns
type ITodoApi = { getAll: unit -> Async<TodoItem list> save: TodoItem -> Async<Result<TodoItem, string>> } type IUserApi = { getCurrent: unit -> Async<User> updateProfile: User -> Async<Result<User, string>> } type IAppApi = { getInfo: unit -> Async<AppInfo> getConfig: unit -> Async<Config> }
Key points:
- One interface per domain area
- Keep APIs focused and cohesive
- Easier to test and maintain
Custom Result Types
Use when: Multiple possible outcomes
type SaveResult = | Created of TodoItem | Updated of TodoItem | ValidationError of string list | Conflict of existingItem: TodoItem type ITodoApi = { save: TodoItem -> Async<SaveResult> }
Type Design Guidelines
✅ Do
Use Records for Data:
type Item = { Id: int Name: string }
Use Option for Nullable:
type User = { Email: string Phone: string option // May not have phone }
Use Result for Fallible Operations:
getById: int -> Async<Result<Item, string>>
Use DateTime from System:
open System type Event = { OccurredAt: DateTime // Serializes correctly }
Descriptive Names:
type OrderStatus = Pending | Confirmed | Shipped | Delivered // NOT: type Status = A | B | C | D
❌ Don't
Don't Use Classes:
// ❌ BAD type Item() = member val Id = 0 with get, set member val Name = "" with get, set // ✅ GOOD type Item = { Id: int; Name: string }
Don't Use Null:
// ❌ BAD type User = { Email: string; Phone: string } // null for no phone? // ✅ GOOD type User = { Email: string; Phone: string option }
Don't Use Nullable<'T>:
// ❌ BAD type User = { Age: Nullable<int> } // ✅ GOOD type User = { Age: int option }
Don't Add Logic to Types:
// ❌ BAD - Keep types pure type User = { Name: string member this.IsValid() = not (String.IsNullOrEmpty this.Name) } // ✅ GOOD - Separate logic type User = { Name: string } module User = let isValid user = not (String.IsNullOrEmpty user.Name)
Common Type Patterns
Timestamps
type Entity = { // ... fields CreatedAt: DateTime UpdatedAt: DateTime }
Soft Delete
type Entity = { // ... fields DeletedAt: DateTime option IsDeleted: bool }
Audit Trail
type Entity = { // ... fields CreatedBy: string CreatedAt: DateTime UpdatedBy: string option UpdatedAt: DateTime option }
Pagination
type PageRequest = { PageNumber: int PageSize: int } type PagedResult<'T> = { Items: 'T list TotalCount: int PageNumber: int PageSize: int TotalPages: int }
Complete Example
// src/Shared/Domain.fs module Shared.Domain open System type Priority = Low | Medium | High type TodoStatus = Active | Completed type TodoItem = { Id: int Title: string Description: string option Priority: Priority Status: TodoStatus CreatedAt: DateTime UpdatedAt: DateTime } type CreateTodoRequest = { Title: string Description: string option Priority: Priority } type TodoList = { Id: int Name: string Items: TodoItem list } // src/Shared/Api.fs module Shared.Api open Domain type ITodoApi = { getAll: unit -> Async<TodoItem list> getActive: unit -> Async<TodoItem list> getById: int -> Async<Result<TodoItem, string>> create: CreateTodoRequest -> Async<Result<TodoItem, string>> complete: int -> Async<Result<TodoItem, string>> delete: int -> Async<Result<unit, string>> } type IListApi = { getAllLists: unit -> Async<TodoList list> getListById: int -> Async<Result<TodoList, string>> addItemToList: listId: int * item: TodoItem -> Async<Result<unit, string>> }
Verification Checklist
- Types defined in
src/Shared/Domain.fs - API contracts in
src/Shared/Api.fs - Used records (not classes)
- Used
for nullable fieldsoption - Used
for fallible operationsResult<'T, string> - All types immutable
- No logic in type definitions
- Meaningful, descriptive names
- Compile succeeds (
)dotnet build
Next Steps
After defining shared types:
- Implement backend with fsharp-backend skill
- Or implement specific layers:
- Validation: fsharp-validation
- Persistence: fsharp-persistence
- Frontend: fsharp-frontend
Related Documentation
Check project docs:
- Detailed type design guide/docs/04-SHARED-TYPES.md
- Quick code templates/docs/09-QUICK-REFERENCE.md
- Project-specific conventionsCLAUDE.md