Claude Code Plugins

Community-maintained marketplace

Feedback

fsharp-feature

@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-feature
description Orchestrates end-to-end F# full-stack feature development across all layers. Use when: "add feature", "implement X", "build Y", "create new feature", "full stack", "new functionality", "add capability", "implement end-to-end", "complete feature". Guides through: Shared types → Backend → Frontend → Tests. Use this for features that touch multiple layers of the application.

F# Full-Stack Feature Development

When to Use This Skill

Activate when:

  • User requests complete new feature ("add todo feature", "implement user management")
  • Need structured guidance through entire stack
  • Building feature from scratch with types, backend, frontend, and tests
  • Project follows F# full-stack blueprint with Elmish + Giraffe

Prerequisites

Project structure:

src/
├── Shared/        # Domain types and API contracts
├── Server/        # Giraffe backend
├── Client/        # Elmish.React + Feliz frontend
└── Tests/         # Expecto tests

Development Process

1. Read Documentation First

Before implementing any feature:

# Check for project-specific patterns
Read: /docs/README.md
Read: /docs/09-QUICK-REFERENCE.md
Read: CLAUDE.md

2. Define Shared Contracts

Location: src/Shared/

Define domain types in Domain.fs:

module Shared.Domain

open System

type TodoItem = {
    Id: int
    Title: string
    Description: string option
    Priority: Priority
    Status: TodoStatus
    CreatedAt: DateTime
    UpdatedAt: DateTime
}

and Priority = Low | Medium | High | Urgent
and TodoStatus = Active | Completed

Define API contract in Api.fs:

module Shared.Api

open Domain

type ITodoApi = {
    getAll: unit -> Async<TodoItem list>
    getById: int -> Async<Result<TodoItem, string>>
    create: CreateTodoRequest -> Async<Result<TodoItem, string>>
    update: TodoItem -> Async<Result<TodoItem, string>>
    delete: int -> Async<Result<unit, string>>
}

type CreateTodoRequest = {
    Title: string
    Description: string option
    Priority: Priority
}

3. Implement Backend

Location: src/Server/

Step 3a: Validation (Validation.fs)

module Validation

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

Step 3b: Domain Logic (Domain.fs - PURE, NO I/O)

module Domain

open System
open Shared.Domain

let processNewTodo (req: CreateTodoRequest) : TodoItem =
    {
        Id = 0  // Set by persistence
        Title = req.Title.Trim()
        Description = req.Description |> Option.map (fun d -> d.Trim())
        Priority = req.Priority
        Status = Active
        CreatedAt = DateTime.UtcNow
        UpdatedAt = DateTime.UtcNow
    }

let completeTodo (todo: TodoItem) : TodoItem =
    { todo with Status = Completed; UpdatedAt = DateTime.UtcNow }

Step 3c: Persistence (Persistence.fs)

module Persistence

open Dapper
open Microsoft.Data.Sqlite
open Shared.Domain

let connectionString = "Data Source=./data/app.db"
let getConnection () = new SqliteConnection(connectionString)

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 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""",
            todo
        ) |> Async.AwaitTask
        return { todo with Id = int id }
    }

Step 3d: API Implementation (Api.fs)

module Api

open Fable.Remoting.Server
open Fable.Remoting.Giraffe
open Shared.Api

let todoApi : ITodoApi = {
    getAll = Persistence.getAllTodos

    getById = fun id -> async {
        match! Persistence.getTodoById id with
        | Some todo -> return Ok todo
        | None -> return Error "Not found"
    }

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

let webApp =
    Remoting.createApi()
    |> Remoting.fromValue todoApi
    |> Remoting.buildHttpHandler

4. Implement Frontend

Location: src/Client/

Step 4a: State Management (State.fs)

module State

open Elmish
open Shared.Domain
open Types

type Model = {
    Todos: RemoteData<TodoItem list>
    NewTodoTitle: string
    NewTodoDescription: string
}

type Msg =
    | LoadTodos
    | TodosLoaded of Result<TodoItem list, string>
    | UpdateNewTodoTitle of string
    | UpdateNewTodoDescription of string
    | CreateTodo
    | TodoCreated of Result<TodoItem, string>

let init () : Model * Cmd<Msg> =
    let model = {
        Todos = NotAsked
        NewTodoTitle = ""
        NewTodoDescription = ""
    }
    model, Cmd.ofMsg LoadTodos

let update (msg: Msg) (model: Model) : Model * Cmd<Msg> =
    match msg with
    | LoadTodos ->
        let cmd = Cmd.OfAsync.either
            Api.todoApi.getAll ()
            (Ok >> TodosLoaded)
            (fun ex -> Error ex.Message |> TodosLoaded)
        { model with Todos = Loading }, cmd

    | TodosLoaded (Ok todos) ->
        { model with Todos = Success todos }, Cmd.none

    | TodosLoaded (Error err) ->
        { model with Todos = Failure err }, Cmd.none

    | UpdateNewTodoTitle title ->
        { model with NewTodoTitle = title }, Cmd.none

    | UpdateNewTodoDescription desc ->
        { model with NewTodoDescription = desc }, Cmd.none

    | CreateTodo ->
        let request = {
            Title = model.NewTodoTitle
            Description = if String.IsNullOrWhiteSpace(model.NewTodoDescription)
                         then None else Some model.NewTodoDescription
            Priority = Medium
        }
        let cmd = Cmd.OfAsync.either
            Api.todoApi.create request
            TodoCreated
            (fun ex -> Error ex.Message |> TodoCreated)
        model, cmd

    | TodoCreated (Ok _) ->
        { model with NewTodoTitle = ""; NewTodoDescription = "" },
        Cmd.ofMsg LoadTodos

    | TodoCreated (Error _) ->
        model, Cmd.none

Step 4b: View (View.fs)

module View

open Feliz
open State
open Types

let private todoCard (todo: TodoItem) (dispatch: Msg -> unit) =
    Html.div [
        prop.className "card bg-base-100 shadow-xl"
        prop.children [
            Html.div [
                prop.className "card-body"
                prop.children [
                    Html.h2 [ prop.className "card-title"; prop.text todo.Title ]
                    match todo.Description with
                    | Some desc -> Html.p [ prop.text desc ]
                    | None -> Html.none
                ]
            ]
        ]
    ]

let view (model: Model) (dispatch: Msg -> unit) =
    Html.div [
        prop.className "container mx-auto p-4"
        prop.children [
            Html.h1 [ prop.className "text-4xl font-bold mb-8"; prop.text "Todos" ]

            // Form
            Html.input [
                prop.className "input input-bordered w-full mb-2"
                prop.placeholder "Title"
                prop.value model.NewTodoTitle
                prop.onChange (UpdateNewTodoTitle >> dispatch)
            ]
            Html.button [
                prop.className "btn btn-primary"
                prop.text "Create"
                prop.onClick (fun _ -> dispatch CreateTodo)
            ]

            // List
            match model.Todos with
            | NotAsked -> Html.div "Loading..."
            | Loading -> Html.span [ prop.className "loading loading-spinner" ]
            | Success todos ->
                Html.div [
                    prop.className "grid grid-cols-3 gap-4 mt-8"
                    prop.children [ for todo in todos -> todoCard todo dispatch ]
                ]
            | Failure err -> Html.div [ prop.className "alert alert-error"; prop.text err ]
        ]
    ]

5. Write Tests

Location: src/Tests/

module Tests.TodoTests

open Expecto
open Shared.Domain

[<Tests>]
let tests =
    testList "Todo Feature" [
        testList "Domain" [
            testCase "processNewTodo trims title" <| fun () ->
                let request = { Title = "  Test  "; Description = None; Priority = Low }
                let result = Domain.processNewTodo request
                Expect.equal result.Title "Test" "Should trim"

            testCase "completeTodo changes status" <| fun () ->
                let todo = { baseTodo with Status = Active }
                let result = Domain.completeTodo todo
                Expect.equal result.Status Completed "Should be completed"
        ]

        testList "Validation" [
            testCase "validates empty title" <| fun () ->
                let request = { Title = ""; Description = None; Priority = Low }
                let result = Validation.validateCreateRequest request
                Expect.isError result "Should fail"
        ]
    ]

Key Principles

Critical Rules:

  1. Type Safety - Define all types in src/Shared/Domain.fs first
  2. Pure Domain - NO I/O in src/Server/Domain.fs (pure functions only)
  3. MVU Pattern - All state changes through update function
  4. Explicit Errors - Use Result<'T, string> for fallible operations
  5. Validate Early - At API boundary before any processing
  6. RemoteData - Use for async operations in frontend state

Development Order:

Shared Types → Backend (Validation → Domain → Persistence → API) → Frontend (State → View) → Tests

Verification Checklist

Before marking feature complete:

  • Types defined in src/Shared/Domain.fs
  • API contract in src/Shared/Api.fs
  • Validation in src/Server/Validation.fs
  • Domain logic pure (no I/O) in src/Server/Domain.fs
  • Persistence in src/Server/Persistence.fs
  • API implementation in src/Server/Api.fs
  • Frontend state (Model/Msg/update) in src/Client/State.fs
  • Frontend view in src/Client/View.fs
  • Tests written (minimum: domain + validation)
  • dotnet build succeeds
  • dotnet test passes

Common Pitfalls

Don't:

  • Put I/O operations in Domain.fs
  • Skip validation
  • Use classes for domain types (use records)
  • Ignore Result/RemoteData error states
  • Start coding without defining types first

Do:

  • Read documentation first
  • Define types before implementation
  • Keep domain logic pure
  • Handle all error cases explicitly
  • Test domain logic thoroughly

Related Skills

For deeper implementation:

  • fsharp-shared - Detailed type patterns
  • fsharp-backend - Backend layer details
  • fsharp-frontend - Frontend patterns
  • fsharp-validation - Complex validation
  • fsharp-persistence - Database patterns
  • fsharp-tests - Testing patterns