Skip to content

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.

Terminal window
go get github.com/fraiseql/fraiseql-go

Requirements: Go 1.22+ (for generics support)

FraiseQL Go provides struct tags and builder types that map your Go types to GraphQL schema constructs:

ConstructGraphQL EquivalentPurpose
fraiseql:"type" struct tagtypeDefine a GraphQL output type
fraiseql:"input" struct taginputDefine a GraphQL input type
fraiseql.Query{...}Query fieldWire a query to a SQL view
fraiseql.Mutation{...}Mutation fieldDefine a mutation

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.

schema.go
package schema
import "github.com/fraiseql/fraiseql-go"
// User is a registered user.
// fraiseql:type
type 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:type
type 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:type
type Comment struct {
ID fraiseql.ID `fraiseql:"id"`
Content string `fraiseql:"content"`
CreatedAt fraiseql.DateTime `fraiseql:"created_at"`
Author User `fraiseql:"author"`
}

FraiseQL provides semantic scalars that add validation and documentation:

import "github.com/fraiseql/fraiseql-go"
// Available scalar types
fraiseql.ID // UUID, auto-serialized
fraiseql.Email // Validated email address
fraiseql.Slug // URL-safe slug
fraiseql.DateTime // ISO 8601 datetime
fraiseql.URL // Validated URL

Use the fraiseql:"input" struct tag to define GraphQL input types for mutations:

schema.go
package schema
import "github.com/fraiseql/fraiseql-go"
// CreateUserInput is the input for creating a user.
// fraiseql:input
type 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:input
type 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:input
type UpdatePostInput struct {
ID fraiseql.ID `fraiseql:"id"`
Title *string `fraiseql:"title"`
Content *string `fraiseql:"content"`
IsPublished *bool `fraiseql:"is_published"`
}

Use fraiseql.Query to wire queries to SQL views. The SQLSource field names the view that backs this query:

schema.go
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.",
},
},
}

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_post
WHERE is_published = true
AND author_id = 'usr_01HZ3K'
LIMIT 20 OFFSET 0;

No resolver code required.


Add mutations to fraiseql.Schema. Each mutation maps to a PostgreSQL function:

schema.go
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.


Use fraiseql.Authenticated and fraiseql.RequiresScope to protect queries and mutations:

schema.go
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"),
),
},
},
}

Use fraiseql.Middleware to intercept requests and set context:

schema.go
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.


A full blog API schema:

schema.go
package schema
import "github.com/fraiseql/fraiseql-go"
// Types
// fraiseql:type
type 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:type
type 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:type
type Comment struct {
ID fraiseql.ID `fraiseql:"id"`
Content string `fraiseql:"content"`
CreatedAt fraiseql.DateTime `fraiseql:"created_at"`
Author User `fraiseql:"author"`
}
// Inputs
// fraiseql:input
type CreatePostInput struct {
Title string `fraiseql:"title" validate:"min=1,max=200"`
Content string `fraiseql:"content"`
IsPublished bool `fraiseql:"is_published"`
}
// fraiseql:input
type 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)
},
},
}

  1. Build the schema — compiles Go types to the FraiseQL IR:

    Terminal window
    fraiseql compile

    Expected output:

    ✓ Schema compiled: 3 types, 2 queries, 2 mutations
    ✓ Views validated against database
    ✓ Build complete: schema.json
  2. Serve the API:

    Terminal window
    fraiseql run

    Expected output:

    ✓ FraiseQL 2.0.0 running on http://localhost:8080/graphql
    ✓ GraphQL Playground at http://localhost:8080/graphql

FraiseQL provides a test client that compiles your schema and runs queries against a real database:

schema_test.go
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)
}
}