Claude-skill-registry fsharp-backend
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-backend" ~/.claude/skills/majiayu000-claude-skill-registry-fsharp-backend && rm -rf "$T"
manifest:
skills/data/fsharp-backend/SKILL.mdsource content
F# Backend Implementation
When to Use This Skill
Activate when:
- User requests "implement backend for X"
- Need to add API endpoints
- Implementing business logic
- Creating server-side functionality
- Project has src/Server/ directory with Giraffe
Architecture Layers
API (Fable.Remoting) ↓ Validation (Input checking) ↓ Domain (Pure business logic - NO I/O) ↓ Persistence (Database/File I/O)
Layer 1: Validation (src/Server/Validation.fs
)
src/Server/Validation.fsPurpose: Validate input at API boundary before processing
For detailed validation patterns, see the fsharp-validation skill.
Basic Pattern
module Validation open Shared.Domain let validateCreateRequest (req: CreateTodoRequest) : Result<CreateTodoRequest, string list> = let errors = [ if String.IsNullOrWhiteSpace(req.Title) then "Title is required" if req.Title.Length > 100 then "Title too long" ] if errors.IsEmpty then Ok req else Error errors
Key points:
- Validate at API boundary, before domain logic
- Return
to accumulate errorsResult<'T, string list> - Convert to single string for API:
String.concat "; " errors
Layer 2: Domain (src/Server/Domain.fs
)
src/Server/Domain.fsPurpose: Pure business logic - NO side effects, NO I/O
Pattern
module Domain open System open Shared.Domain // ✅ PURE transformations only let processNewTodo (request: CreateTodoRequest) : TodoItem = { Id = 0 // Will be set by persistence Title = request.Title.Trim() Description = request.Description |> Option.map (fun d -> d.Trim()) Priority = request.Priority Status = Active CreatedAt = DateTime.UtcNow UpdatedAt = DateTime.UtcNow } let completeTodo (item: TodoItem) : TodoItem = { item with Status = Completed; UpdatedAt = DateTime.UtcNow } let updateTodo (existing: TodoItem) (updates: CreateTodoRequest) : TodoItem = { existing with Title = updates.Title.Trim() Description = updates.Description |> Option.map (fun d -> d.Trim()) Priority = updates.Priority UpdatedAt = DateTime.UtcNow } // ✅ PURE calculations let calculatePriority (items: TodoItem list) : Priority = items |> List.map (fun item -> item.Priority) |> List.maxBy (function Urgent -> 4 | High -> 3 | Medium -> 2 | Low -> 1)
CRITICAL:
// ❌ BAD - I/O in domain let processItem itemId = let item = Persistence.getItem itemId // NO! { item with Status = Processed } // ✅ GOOD - Pure function let processItem item = { item with Status = Processed }
Layer 3: Persistence (src/Server/Persistence.fs
)
src/Server/Persistence.fsPurpose: All database and file I/O operations
SQLite with Dapper
module Persistence open System.Data open Microsoft.Data.Sqlite open Dapper open Shared.Domain let connectionString = "Data Source=./data/app.db" let getConnection () = new SqliteConnection(connectionString) let initializeDatabase () = use conn = getConnection() conn.Execute(""" CREATE TABLE IF NOT EXISTS TodoItems ( Id INTEGER PRIMARY KEY AUTOINCREMENT, Title TEXT NOT NULL, Description TEXT, Priority INTEGER NOT NULL, Status INTEGER NOT NULL, CreatedAt TEXT NOT NULL, UpdatedAt TEXT NOT NULL ) """) |> ignore // READ operations let getAllTodos () : Async<TodoItem list> = async { use conn = getConnection() let! todos = conn.QueryAsync<TodoItem>( "SELECT * FROM TodoItems ORDER BY CreatedAt DESC" ) |> Async.AwaitTask return todos |> Seq.toList } let getTodoById (id: int) : Async<TodoItem option> = async { use conn = getConnection() let! todo = conn.QuerySingleOrDefaultAsync<TodoItem>( "SELECT * FROM TodoItems WHERE Id = @Id", {| Id = id |} ) |> Async.AwaitTask return if isNull (box todo) then None else Some todo } // WRITE operations let insertTodo (todo: TodoItem) : Async<TodoItem> = async { use conn = getConnection() let! id = conn.ExecuteScalarAsync<int64>( """INSERT INTO TodoItems (Title, Description, Priority, Status, CreatedAt, UpdatedAt) VALUES (@Title, @Description, @Priority, @Status, @CreatedAt, @UpdatedAt) RETURNING Id""", {| Title = todo.Title Description = todo.Description Priority = int todo.Priority Status = int todo.Status CreatedAt = todo.CreatedAt.ToString("o") UpdatedAt = todo.UpdatedAt.ToString("o") |} ) |> Async.AwaitTask return { todo with Id = int id } } let updateTodo (todo: TodoItem) : Async<unit> = async { use conn = getConnection() let! _ = conn.ExecuteAsync( """UPDATE TodoItems SET Title = @Title, Description = @Description, Priority = @Priority, Status = @Status, UpdatedAt = @UpdatedAt WHERE Id = @Id""", todo ) |> Async.AwaitTask return () } let deleteTodo (id: int) : Async<unit> = async { use conn = getConnection() let! _ = conn.ExecuteAsync( "DELETE FROM TodoItems WHERE Id = @Id", {| Id = id |} ) |> Async.AwaitTask return () }
Key points:
- Use parameterized queries (SQL injection prevention)
- Always use
for I/Oasync - Dispose connections (
keyword)use - Return options for "not found" cases
Layer 4: API (src/Server/Api.fs
)
src/Server/Api.fsPurpose: Implement Fable.Remoting contracts, orchestrate layers
Pattern
module Api open Fable.Remoting.Server open Fable.Remoting.Giraffe open Shared.Api open Shared.Domain let todoApi : ITodoApi = { getAll = fun () -> Persistence.getAllTodos() getById = fun id -> async { match! Persistence.getTodoById id with | Some todo -> return Ok todo | None -> return Error "Todo not found" } create = fun request -> async { // 1. Validate match Validation.validateCreateRequest request with | Error errors -> return Error (String.concat "; " errors) | Ok validRequest -> // 2. Process with domain logic let newTodo = Domain.processNewTodo validRequest // 3. Persist let! saved = Persistence.insertTodo newTodo return Ok saved } update = fun todo -> async { // 1. Validate match Validation.validateTodoItem todo with | Error errors -> return Error (String.concat "; " errors) | Ok validTodo -> // 2. Check exists match! Persistence.getTodoById todo.Id with | None -> return Error "Todo not found" | Some existing -> // 3. Process with domain logic let updated = Domain.updateTodo existing validTodo // 4. Persist do! Persistence.updateTodo updated return Ok updated } complete = fun id -> async { match! Persistence.getTodoById id with | None -> return Error "Todo not found" | Some todo -> let completed = Domain.completeTodo todo do! Persistence.updateTodo completed return Ok completed } delete = fun id -> async { do! Persistence.deleteTodo id return Ok () } } // Create HTTP handler let webApp = Remoting.createApi() |> Remoting.fromValue todoApi |> Remoting.buildHttpHandler
Orchestration pattern:
- Validate input
- Return error if invalid
- Transform with domain logic (pure)
- Perform persistence operations
- Return result
Error Handling Patterns
Not Found:
getById = fun id -> async { match! Persistence.getById id with | Some item -> return Ok item | None -> return Error "Not found" }
Validation Errors:
save = fun item -> async { match Validation.validate item with | Error errs -> return Error (String.concat "; " errs) | Ok valid -> do! Persistence.save valid return Ok valid }
Exception Handling:
save = fun item -> async { try do! Persistence.save item return Ok item with | ex -> return Error $"Failed to save: {ex.Message}" }
Integration with Program.fs
module Program open Microsoft.AspNetCore.Builder open Giraffe let configureApp (app: IApplicationBuilder) = // Initialize persistence Persistence.ensureDataDir() Persistence.initializeDatabase() app.UseStaticFiles() |> ignore app.UseRouting() |> ignore app.UseGiraffe(Api.webApp) [<EntryPoint>] let main args = let builder = WebApplication.CreateBuilder(args) builder.Services.AddGiraffe() |> ignore let app = builder.Build() configureApp app app.Run() 0
Complete Example
// Validation.fs module Validation open Shared.Domain let validateCreateRequest (req: CreateTodoRequest) = let errors = [ if String.IsNullOrWhiteSpace(req.Title) then "Title required" if req.Title.Length > 100 then "Title too long" ] if errors.IsEmpty then Ok req else Error errors // Domain.fs module Domain open System open Shared.Domain let processNewTodo (req: CreateTodoRequest) : TodoItem = { Id = 0 Title = req.Title.Trim() Description = req.Description Priority = req.Priority Status = Active CreatedAt = DateTime.UtcNow UpdatedAt = DateTime.UtcNow } // Persistence.fs (SQLite example above) // Api.fs module Api open Shared.Api let todoApi : ITodoApi = { create = fun request -> async { match Validation.validateCreateRequest request with | Error errs -> return Error (String.concat "; " errs) | Ok valid -> let todo = Domain.processNewTodo valid let! saved = Persistence.insertTodo todo return Ok saved } }
Verification Checklist
- Validation in
src/Server/Validation.fs - Domain logic in
(PURE - no I/O)src/Server/Domain.fs - Persistence in
src/Server/Persistence.fs - API implementation in
src/Server/Api.fs - Proper error handling (Result types)
- Database initialized (if using SQLite)
- All async operations used for I/O
- No I/O in domain layer
- Parameterized queries (SQL injection prevention)
Common Pitfalls
❌ Don't:
- Put database calls in Domain.fs
- Skip validation
- Use string concatenation for SQL queries
- Forget to dispose database connections
- Mix concerns across layers
✅ Do:
- Keep domain logic pure
- Validate at API boundary
- Use parameterized queries
- Handle all error cases explicitly
- Follow layer responsibilities strictly
Related Skills
- fsharp-validation - Detailed validation patterns
- fsharp-persistence - More persistence patterns
- fsharp-tests - Testing backend logic
- fsharp-shared - Type definitions
Related Documentation
- Detailed backend guide/docs/03-BACKEND-GUIDE.md
- Persistence patterns/docs/05-PERSISTENCE.md
- Quick templates/docs/09-QUICK-REFERENCE.md