Full Tutorial
Go SDK
The FraiseQL Go SDK is a schema authoring SDK: you define GraphQL types, queries, and mutations in Go using struct tags and builder patterns, and the FraiseQL compiler generates an optimized GraphQL API backed by your SQL views.
Installation
Section titled “Installation”go get github.com/fraiseql/fraiseql-goRequirements: Go 1.22+ (for generics support)
Core Concepts
Section titled “Core Concepts”FraiseQL Go provides struct tags and builder types that map your Go types to GraphQL schema constructs:
| Construct | GraphQL Equivalent | Purpose |
|---|---|---|
fraiseql:"type" struct tag | type | Define a GraphQL output type |
fraiseql:"input" struct tag | input | Define a GraphQL input type |
fraiseql.Query{...} | Query field | Wire a query to a SQL view |
fraiseql.Mutation{...} | Mutation field | Define a mutation |
Defining Types
Section titled “Defining Types”Use the fraiseql:"type" struct tag to define GraphQL output types. Fields map 1:1 to columns in your backing SQL view’s .data JSONB object.
package schema
import "github.com/fraiseql/fraiseql-go"
// User is a registered user.// fraiseql:typetype User struct { ID fraiseql.ID `fraiseql:"id"` Username string `fraiseql:"username"` Email fraiseql.Email `fraiseql:"email"` Bio *string `fraiseql:"bio"` CreatedAt fraiseql.DateTime `fraiseql:"created_at"`}
// Post is a blog post.// fraiseql:typetype Post struct { ID fraiseql.ID `fraiseql:"id"` Title string `fraiseql:"title"` Slug fraiseql.Slug `fraiseql:"slug"` Content string `fraiseql:"content"` IsPublished bool `fraiseql:"is_published"` CreatedAt fraiseql.DateTime `fraiseql:"created_at"` UpdatedAt fraiseql.DateTime `fraiseql:"updated_at"` // Nested types are composed from views at compile time Author User `fraiseql:"author"` Comments []Comment `fraiseql:"comments"`}
// Comment is a comment on a post.// fraiseql:typetype Comment struct { ID fraiseql.ID `fraiseql:"id"` Content string `fraiseql:"content"` CreatedAt fraiseql.DateTime `fraiseql:"created_at"` Author User `fraiseql:"author"`}Built-in Scalars
Section titled “Built-in Scalars”FraiseQL provides semantic scalars that add validation and documentation:
import "github.com/fraiseql/fraiseql-go"
// Available scalar typesfraiseql.ID // UUID, auto-serializedfraiseql.Email // Validated email addressfraiseql.Slug // URL-safe slugfraiseql.DateTime // ISO 8601 datetimefraiseql.URL // Validated URLDefining Inputs
Section titled “Defining Inputs”Use the fraiseql:"input" struct tag to define GraphQL input types for mutations:
package schema
import "github.com/fraiseql/fraiseql-go"
// CreateUserInput is the input for creating a user.// fraiseql:inputtype CreateUserInput struct { Username string `fraiseql:"username" validate:"min=3,max=50,pattern=^[a-z0-9_]+$"` Email fraiseql.Email `fraiseql:"email" validate:"required"` Bio *string `fraiseql:"bio"`}
// CreatePostInput is the input for creating a post.// fraiseql:inputtype CreatePostInput struct { Title string `fraiseql:"title" validate:"min=1,max=200"` Content string `fraiseql:"content"` AuthorID fraiseql.ID `fraiseql:"author_id"` IsPublished bool `fraiseql:"is_published"`}
// UpdatePostInput is the input for updating a post.// fraiseql:inputtype UpdatePostInput struct { ID fraiseql.ID `fraiseql:"id"` Title *string `fraiseql:"title"` Content *string `fraiseql:"content"` IsPublished *bool `fraiseql:"is_published"`}Defining Queries
Section titled “Defining Queries”Use fraiseql.Query to wire queries to SQL views. The SQLSource field names the view that backs this query:
package schema
import "github.com/fraiseql/fraiseql-go"
// Schema defines all queries and mutations.var Schema = fraiseql.Schema{ Queries: []fraiseql.Query{ // List query — maps to SELECT * FROM v_post WHERE <args> { Name: "posts", SQLSource: "v_post", Returns: []Post{}, Args: fraiseql.Args{ "is_published": (*bool)(nil), "author_id": (*fraiseql.ID)(nil), "limit": 20, "offset": 0, }, Description: "Fetch published posts, optionally filtered by author.", }, // Single-item query — maps to SELECT * FROM v_post WHERE id = $1 { Name: "post", SQLSource: "v_post", IDArg: "id", Returns: (*Post)(nil), Description: "Fetch a single post by ID.", }, // Query with row filter for current user's posts { Name: "my_posts", SQLSource: "v_post", RowFilter: "author_id = {current_user_id}", Returns: []Post{}, Args: fraiseql.Args{ "limit": 20, }, Description: "Fetch the current user's posts.", }, },}How Arguments Become WHERE Clauses
Section titled “How Arguments Become WHERE Clauses”FraiseQL’s automatic-where feature maps query arguments to SQL filters automatically. Declare an argument whose name matches a column in the backing view, and FraiseQL appends it as a WHERE clause:
query { posts(is_published: true, author_id: "usr_01HZ3K") { ... }}Becomes:
SELECT data FROM v_postWHERE is_published = true AND author_id = 'usr_01HZ3K'LIMIT 20 OFFSET 0;No resolver code required.
Defining Mutations
Section titled “Defining Mutations”Add mutations to fraiseql.Schema. Each mutation maps to a PostgreSQL function:
package schema
import "github.com/fraiseql/fraiseql-go"
var Schema = fraiseql.Schema{ // ...queries above... Mutations: []fraiseql.Mutation{ { Name: "create_user", Input: CreateUserInput{}, Returns: User{}, Description: "Create a new user account.", // FraiseQL calls: SELECT * FROM fn_create_user($1::jsonb) }, { Name: "create_post", Input: CreatePostInput{}, Returns: Post{}, Auth: fraiseql.RequiresScope("write:posts"), Description: "Create a new blog post.", }, { Name: "publish_post", Args: fraiseql.Args{"id": fraiseql.ID("")}, Returns: Post{}, Auth: fraiseql.RequiresScope("write:posts"), Description: "Publish a draft post.", }, { Name: "delete_post", Args: fraiseql.Args{"id": fraiseql.ID("")}, Returns: false, Auth: fraiseql.RequiresScope("admin:posts"), Description: "Soft-delete a post.", }, },}Each mutation maps to a PostgreSQL function in db/schema/03_functions/. The Go definition is the schema; the SQL function is the implementation.
Authorization
Section titled “Authorization”Use fraiseql.Authenticated and fraiseql.RequiresScope to protect queries and mutations:
var Schema = fraiseql.Schema{ Queries: []fraiseql.Query{ { Name: "posts", SQLSource: "v_post", Returns: []Post{}, Auth: fraiseql.Authenticated, }, }, Mutations: []fraiseql.Mutation{ { Name: "create_post", Input: CreatePostInput{}, Returns: Post{}, Auth: fraiseql.All( fraiseql.Authenticated, fraiseql.RequiresScope("write:posts"), ), }, },}Middleware
Section titled “Middleware”Use fraiseql.Middleware to intercept requests and set context:
package schema
import "github.com/fraiseql/fraiseql-go"
var Middleware = []fraiseql.MiddlewareFunc{ // Extract user ID and org from verified JWT func(req *fraiseql.Request, next fraiseql.Handler) (*fraiseql.Response, error) { if req.Auth != nil { req.Context["current_user_id"] = req.Auth.Claims["sub"] req.Context["current_org_id"] = req.Auth.Claims["org_id"] } return next(req) },}Values set on req.Context are available in RowFilter expressions and in mutation context.
Complete Schema Example
Section titled “Complete Schema Example”A full blog API schema:
package schema
import "github.com/fraiseql/fraiseql-go"
// Types
// fraiseql:typetype User struct { ID fraiseql.ID `fraiseql:"id"` Username string `fraiseql:"username"` Email fraiseql.Email `fraiseql:"email"` Bio *string `fraiseql:"bio"` CreatedAt fraiseql.DateTime `fraiseql:"created_at"`}
// fraiseql:typetype Post struct { ID fraiseql.ID `fraiseql:"id"` Title string `fraiseql:"title"` Slug fraiseql.Slug `fraiseql:"slug"` Content string `fraiseql:"content"` IsPublished bool `fraiseql:"is_published"` CreatedAt fraiseql.DateTime `fraiseql:"created_at"` UpdatedAt fraiseql.DateTime `fraiseql:"updated_at"` Author User `fraiseql:"author"` Comments []Comment `fraiseql:"comments"`}
// fraiseql:typetype Comment struct { ID fraiseql.ID `fraiseql:"id"` Content string `fraiseql:"content"` CreatedAt fraiseql.DateTime `fraiseql:"created_at"` Author User `fraiseql:"author"`}
// Inputs
// fraiseql:inputtype CreatePostInput struct { Title string `fraiseql:"title" validate:"min=1,max=200"` Content string `fraiseql:"content"` IsPublished bool `fraiseql:"is_published"`}
// fraiseql:inputtype CreateCommentInput struct { PostID fraiseql.ID `fraiseql:"post_id"` Content string `fraiseql:"content" validate:"min=1,max=10000"`}
// Schema
var Schema = fraiseql.Schema{ Queries: []fraiseql.Query{ {Name: "posts", SQLSource: "v_post", Returns: []Post{}, Args: fraiseql.Args{"is_published": (*bool)(nil), "limit": 20}}, {Name: "post", SQLSource: "v_post", IDArg: "id", Returns: (*Post)(nil)}, }, Mutations: []fraiseql.Mutation{ {Name: "create_post", Input: CreatePostInput{}, Returns: Post{}, Auth: fraiseql.All(fraiseql.Authenticated, fraiseql.RequiresScope("write:posts"))}, {Name: "create_comment", Input: CreateCommentInput{}, Returns: Comment{}, Auth: fraiseql.All(fraiseql.Authenticated, fraiseql.RequiresScope("write:comments"))}, }, Middleware: []fraiseql.MiddlewareFunc{ func(req *fraiseql.Request, next fraiseql.Handler) (*fraiseql.Response, error) { if req.Auth != nil { req.Context["current_user_id"] = req.Auth.Claims["sub"] } return next(req) }, },}Build and Serve
Section titled “Build and Serve”-
Build the schema — compiles Go types to the FraiseQL IR:
Terminal window fraiseql compileExpected output:
✓ Schema compiled: 3 types, 2 queries, 2 mutations✓ Views validated against database✓ Build complete: schema.json -
Serve the API:
Terminal window fraiseql runExpected output:
✓ FraiseQL 2.0.0 running on http://localhost:8080/graphql✓ GraphQL Playground at http://localhost:8080/graphql
Testing
Section titled “Testing”FraiseQL provides a test client that compiles your schema and runs queries against a real database:
package schema_test
import ( "testing" "github.com/fraiseql/fraiseql-go/testing")
func TestCreateAndFetchPost(t *testing.T) { client := fraiseqltest.NewClient(t, fraiseqltest.Config{ SchemaPath: "./schema.go", DatabaseURL: os.Getenv("FRAISEQL_TEST_DATABASE_URL"), })
// Create a post result, err := client.Mutate(t, ` mutation { createPost(input: { title: "Hello", content: "World" }) { id title isPublished } } `) if err != nil { t.Fatal(err) }
post := result["createPost"].(map[string]any) if post["title"] != "Hello" { t.Errorf("expected title Hello, got %v", post["title"]) }
// Fetch it back postID := post["id"].(string) result, err = client.Query(t, ` query { post(id: "`+postID+`") { title content } } `) if err != nil { t.Fatal(err) }}Next Steps
Section titled “Next Steps”Custom Queries
Security
Other SDKs