| 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
asyncfor I/O - Dispose connections (
usekeyword) - 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:
- 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
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