fsharp-shared
Define shared domain types and API contracts for F# full-stack applications.Use when: "define types", "create entity", "add model", "API contract", "shared types","domain model", "record", "discriminated union", "data structure", "DTO","interface", "type definition", "new entity", "modify types".Creates types in src/Shared/Domain.fs and API interfaces in src/Shared/Api.fs.Always start here when adding new features.
When & Why to Use This Skill
This Claude skill streamlines F# full-stack development by automating the definition of shared domain types and API contracts. It ensures architectural consistency and type safety across client and server layers by managing records, discriminated unions, and interface definitions in a centralized shared project, following best practices for Fable.Remoting and functional domain modeling.
Use Cases
- Feature Initialization: Defining core domain entities and data structures at the start of a new feature to ensure a 'types-first' development workflow.
- Contract-First Development: Establishing type-safe API interfaces between Fable frontends and .NET backends to eliminate communication mismatches.
- Domain Modeling: Implementing complex business logic using Discriminated Unions and Records to create self-documenting, immutable data structures.
- Validation Logic: Creating 'Smart Constructors' and constrained types to enforce business rules and data integrity directly within the type system.
- Refactoring: Modifying existing shared models and DTOs while maintaining synchronization between the frontend and backend codebases.
| name | fsharp-shared |
|---|---|
| description | | |
| Use when | "define types", "create entity", "add model", "API contract", "shared types", |
| allowed-tools | Read, Edit, Write, Grep, Glob |
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:
src/Shared/Domain.fsfor domain typessrc/Shared/Api.fsfor API contracts- 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
optionfor nullable fields - 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:
Async<'T list>- Always returns (empty list if none)Async<Result<'T, string>>- May fail (not found, validation error)Async<Result<unit, string>>- Success with no data to return
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
optionfor nullable fields - Used
Result<'T, string>for fallible operations - 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:
/docs/04-SHARED-TYPES.md- Detailed type design guide/docs/09-QUICK-REFERENCE.md- Quick code templatesCLAUDE.md- Project-specific conventions