Claude Code Plugins

Community-maintained marketplace

Feedback

fsharp-backend

@heimeshoff/Cinemarco
1
0

|

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name fsharp-backend
description Implement F# backend using Giraffe + Fable.Remoting with proper layer separation. Use when: "implement backend", "add API", "create endpoint", "server logic", "business rules", "backend for X", "API implementation", "server-side", "Giraffe", "Fable.Remoting". Layers: Validation → Domain (pure) → Persistence (I/O) → API. Creates code in src/Server/: Validation.fs, Domain.fs, Persistence.fs, Api.fs.
allowed-tools Read, Edit, Write, Grep, Glob, Bash

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)

Purpose: 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 Result<'T, string list> to accumulate errors
  • Convert to single string for API: String.concat "; " errors

Layer 2: Domain (src/Server/Domain.fs)

Purpose: 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)

Purpose: 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 async for I/O
  • Dispose connections (use keyword)
  • Return options for "not found" cases

Layer 4: API (src/Server/Api.fs)

Purpose: 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:

  1. Validate input
  2. Return error if invalid
  3. Transform with domain logic (pure)
  4. Perform persistence operations
  5. 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 src/Server/Domain.fs (PURE - no I/O)
  • 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

  • /docs/03-BACKEND-GUIDE.md - Detailed backend guide
  • /docs/05-PERSISTENCE.md - Persistence patterns
  • /docs/09-QUICK-REFERENCE.md - Quick templates