Skip to content

F# SDK

The FraiseQL F# SDK is a schema authoring SDK: you define GraphQL types, queries, and mutations using F# computation expressions, and the FraiseQL compiler generates an optimized GraphQL API backed by your SQL views.

Terminal window
dotnet add package FraiseQL.FSharp

Requirements: .NET 8+ / F# 7+


FraiseQL F# uses computation expressions — F#‘s idiomatic DSL mechanism — to define schema constructs without global mutable state:

BuilderGraphQL EquivalentPurpose
fraiseql { type' ... }typeAccumulate type and query definitions into a schema
TypeCEBuilderfield declarationsDefine a GraphQL output or input type
QueryCEBuilderQuery fieldWire a query to a SQL view
MutationCEBuilderMutation fieldDefine a mutation
FieldBuilderfield metadataConfigure a single field’s type and nullability

All definitions are composed into an IntermediateSchema value using the fraiseql { } computation expression:

Schema.fs
open FraiseQL
let schema =
fraiseql {
type' "User" (TypeCEBuilder("User") {
sqlSource "v_user"
description "A registered user"
field "id" "ID" { nullable false }
field "username" "String" { nullable false }
field "email" "Email" { nullable false }
field "bio" "String" { nullable true }
field "createdAt" "DateTime" { nullable false }
})
type' "Post" (TypeCEBuilder("Post") {
sqlSource "v_post"
field "id" "ID" { nullable false }
field "title" "String" { nullable false }
field "slug" "Slug" { nullable false }
field "content" "String" { nullable false }
field "isPublished" "Boolean" { nullable false }
field "createdAt" "DateTime" { nullable false }
})
}

Input types use isInput true on TypeCEBuilder:

Schema.fs
let schema =
fraiseql {
type' "CreatePostInput" (TypeCEBuilder("CreatePostInput") {
isInput true
field "title" "String" { nullable false }
field "content" "String" { nullable false }
field "isPublished" "Boolean" { nullable false }
})
}

Use QueryCEBuilder inside the fraiseql { } block:

Schema.fs
let schema =
fraiseql {
// List query
query "posts" (QueryCEBuilder("posts") {
returnType "Post"
returnsList true
sqlSource "v_post"
description "Fetch posts, optionally filtered by published status"
arg "isPublished" "Boolean" { nullable true }
arg "limit" "Int" { defaultValue 20 }
arg "offset" "Int" { defaultValue 0 }
})
// Single-item query
query "post" (QueryCEBuilder("post") {
returnType "Post"
sqlSource "v_post"
nullable true
arg "id" "ID" { nullable false }
})
}

Query arguments matching columns in the backing view become SQL WHERE clauses automatically. See SQL Patterns → Automatic WHERE Clauses for details.


Use MutationCEBuilder inside the fraiseql { } block:

Schema.fs
let schema =
fraiseql {
mutation "createPost" (MutationCEBuilder("createPost") {
sqlSource "fn_create_post"
returnType "Post"
operation "insert"
invalidatesViews [ "v_post" ]
description "Create a new blog post"
arg "input" "CreatePostInput" { nullable false }
})
mutation "deletePost" (MutationCEBuilder("deletePost") {
sqlSource "fn_delete_post"
returnType "Post"
operation "delete"
requiresRole "admin"
invalidatesViews [ "v_post" ]
arg "id" "ID" { nullable false }
})
}

Mutations wire to PostgreSQL functions returning mutation_response. See SQL Patterns for complete function templates.


Use requiresRole on query or mutation builders:

query "myPosts" (QueryCEBuilder("myPosts") {
sqlSource "v_post"
returnType "Post"
returnsList true
requiresRole "member"
injectParams [ "authorId", "jwt:sub" ]
})

Field-level scope restrictions use scope in FieldBuilder:

type' "User" (TypeCEBuilder("User") {
sqlSource "v_user"
field "id" "ID" { nullable false }
field "email" "Email" { nullable false; scope "user:read:email" }
})

Transport annotations are optional. Omit them to serve an operation via GraphQL only. Use [<Query(...)>] and [<Mutation(...)>] .NET attributes with SqlSource, RestPath, and RestMethod named parameters. gRPC endpoints are auto-generated when [grpc] is enabled — no per-operation annotation needed. See gRPC Transport.

open FraiseQL.SDK
[<Query(SqlSource = "v_post", RestPath = "/posts", RestMethod = "GET")>]
let posts (limit: int) : Post list = []
[<Query(SqlSource = "v_post", RestPath = "/posts/{id}", RestMethod = "GET")>]
let post (id: System.Guid) : Post option = None
[<Mutation(SqlSource = "create_post", Operation = "CREATE",
RestPath = "/posts", RestMethod = "POST")>]
let createPost (title: string) (authorId: System.Guid) : Post = Unchecked.defaultof<Post>

Path parameters in RestPath (e.g., {id}) must match function parameter names exactly. A mismatch produces a compile-time error. Duplicate (method, path) pairs are also rejected at compile time.


  1. Export the schema — converts your IntermediateSchema to schema.json:

    Export/Program.fs
    open FraiseQL
    open MyApp
    [<EntryPoint>]
    let main _ =
    Schema.schema
    |> SchemaExporter.exportToFile "schema.json"
    0

    Run the export:

    Terminal window
    dotnet run --project Export
  2. Compile with the FraiseQL CLI:

    Terminal window
    fraiseql compile --schema schema.json
  3. Serve the API:

    Terminal window
    fraiseql run

FraiseQLClient provides async GraphQL queries and mutations from F# applications:

open FraiseQL
let options =
{ Endpoint = System.Uri "https://api.example.com/graphql"
Authorization = Some "Bearer your-token"
Retry = Some { MaxAttempts = 3; BaseDelay = 200; MaxDelay = 5000; Jitter = true } }
use client = new FraiseQLClient(options)
// Query
let! users = client.QueryAsync<User list> """
query { users { id username email } }
"""
// Mutation
let! post = client.MutateAsync<Post>(
"""mutation($input: CreatePostInput!) { createPost(input: $input) { id title } }""",
{| input = {| title = "Hello"; content = "World"; isPublished = true |} |})

The exception hierarchy (FraiseQLException, GraphQLException, AuthenticationException, RateLimitException, FraiseQLTimeoutException, NetworkException) is identical to the C# SDK.