Integration Guide
Go (net/http and Gin) integration guide
Go teams shipping REST APIs often maintain strong contracts enforced through OpenAPI specifications, hand-maintained documentation, or generated client libraries. StateAnchor gates those contracts in CI -- detecting breaking changes in your route signatures and struct fields before SDK consumers are affected.
This guide covers APIs built with the standard net/http package, Gin, or Chi. StateAnchor does not integrate with your Go code directly. It reads thestateanchor.yaml file that describes your API contract and diffs it on every push.
What you’ll need
- Go 1.21 or later
- A GitHub repository
- A running REST API -- works with
net/http, Gin, Chi, or any framework - A StateAnchor account -- connect a repo to start
Step 1 -- Map your API to stateanchor.yaml
Create a stateanchor.yaml in the root of your repository (or at the service root in a monorepo). StateAnchor does not care which Go HTTP framework you use -- it only reads the contract you declare.
Here is a complete example for a User Service:
service: user-service
version: "1.0.0"
info:
title: User Service
description: Go REST API for user management
server:
base_url: https://api.acme.com
auth:
type: bearer
models:
User:
id: integer
name: string
email: string
created_at: string
CreateUserRequest:
name: string
email: string
endpoints:
- name: listUsers
method: GET
path: /api/v1/users
description: List all users
- name: getUser
method: GET
path: /api/v1/users/{id}
description: Get a user by ID
returns:
$ref: User
- name: createUser
method: POST
path: /api/v1/users
description: Create a new user
body:
$ref: CreateUserRequest
returns:
$ref: User
- name: updateUser
method: PUT
path: /api/v1/users/{id}
description: Update a user
body:
$ref: CreateUserRequest
returns:
$ref: User
- name: deleteUser
method: DELETE
path: /api/v1/users/{id}
description: Delete a user
outputs:
go: true
typescript: trueA few Go-specific notes:
- Model names -- use idiomatic Go PascalCase:
User,CreateUserRequest,UserResponse. These names appear in the generated Go client as struct types. - Path parameters -- use braces regardless of framework. Gin uses
:idin its router, but write/api/v1/users/{id}in StateAnchor. The spec describes the HTTP contract, not the router syntax. - outputs -- set
go: trueto generate a typed Go client library. Settypescript: trueas well if your frontend also consumes the API.
Step 2 -- Add the GitHub Action
Add the StateAnchor GitHub Action alongside your existing Go CI steps. Go repos often run tests and linting in CI -- the StateAnchor gate can run in parallel with those jobs.
Create .github/workflows/stateanchor.yml:
name: StateAnchor gate
on:
push:
branches: ["main", "master"]
pull_request:
permissions:
contents: read
statuses: write
pull-requests: write
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.21"
- run: go test ./...
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: StateAnchor sync
uses: stateanchor/sync-action@v1
with:
api-key: ${{ secrets.STATEANCHOR_API_KEY }}stateanchor.yaml. Use the config-pathAction input to point at each service’s spec file. See the monorepo pattern below.Step 3 -- ERR patterns common in Go APIs
Go’s strong typing means API contracts are often carefully maintained -- but a few patterns reliably trigger breaking changes:
Exported struct field becomes unexported (or removed)
You change a struct field from ID int64 to a private id int64, or you remove a field that was previously in a JSON response body. StateAnchor emitsfield_removed. Consumers that deserialize the JSON response will fail to populate the field.
Integer width change (int32 → int64)
You change an ID field from int32 to int64 to support larger values. StateAnchor emits type_changed. Generated Go and TypeScript clients will have the wrong numeric type, and consumers using the old type will fail at deserialization for values that exceed int32 range.
Required query parameter added to a GET endpoint
You add a required tenant_id query parameter toGET /api/v1/users. StateAnchor emits required_param_added. Existing callers that do not include this parameter will start receiving 400 Bad Request.
Common patterns
Monorepo services
If your monorepo contains multiple Go services (e.g., services/users/,services/payments/), each service should have its ownstateanchor.yaml committed at the service root. Use the config-pathinput in the GitHub Action to point at each one, and create a separate workflow job (or workflow file) per service:
jobs:
gate-users:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: stateanchor/sync-action@v1
with:
api-key: ${{ secrets.STATEANCHOR_API_KEY }}
config-path: services/users/stateanchor.yaml
gate-payments:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: stateanchor/sync-action@v1
with:
api-key: ${{ secrets.STATEANCHOR_API_KEY }}
config-path: services/payments/stateanchor.yamlgRPC-gateway APIs
If you expose gRPC services via HTTP transcoding with grpc-gateway, map the HTTP endpoints (not the proto methods) to StateAnchor. Use the HTTP path pattern from your google.api.http annotations, not the RPC method name:
# The proto method is GetUser(GetUserRequest) returns (User)
# The HTTP transcoding rule is: GET /v1/users/{id}
# Use the HTTP path in stateanchor.yaml:
endpoints:
- name: getUser
method: GET
path: /v1/users/{id}
description: Get a user (transcoded from GetUser RPC)
returns:
$ref: UserVersioning via package paths (/v2)
The Go module versioning convention uses /v2 in import paths for breaking changes. Apply the same principle to StateAnchor: treat each major API version as a separate StateAnchor project with its own spec file. When you release v2, createstateanchor-v2.yaml rather than modifying the v1 spec. This preserves the gate history and contract for consumers who have not migrated:
# stateanchor.yaml -- v1 spec, never modified after v2 ships
service: user-service-v1
version: "1.0.0"
server:
base_url: https://api.acme.com
auth:
type: bearer
endpoints:
- name: listUsers
method: GET
path: /v1/users
outputs:
go: trueNext steps
- Drift exceptions -- approve intentional breaking changes with the two-signal model
- Gate engine -- how the four drift syndromes drive the gate decision
- stateanchor.yaml reference -- complete field-by-field spec documentation