Tutorial¶
This tutorial walks through the complete APX workflow: authoring schemas, local development with canonical imports, releasing to the canonical repo, and consuming APIs from other services.
Prerequisites
Complete the Quick Start first — you should have a canonical repo and an app repo initialized.
Path Mapping Reference¶
Every API in the canonical repo maps to a deterministic set of coordinates:
| Coordinate | Example |
|---|---|
| API ID | proto/payments/ledger/v1 |
| Source path | proto/payments/ledger/v1 |
| Proto package | myorg.payments.ledger.v1 |
| Go module (v1) | github.com/myorg/apis/proto/payments/ledger |
| Go import (v1) | github.com/myorg/apis/proto/payments/ledger/v1 |
| Go module (v2) | github.com/myorg/apis/proto/payments/ledger/v2 |
| Go import (v2) | github.com/myorg/apis/proto/payments/ledger/v2 |
| Git tag | proto/payments/ledger/v1/v1.2.3 |
One canonical repo. One default import root. One path model.
Tip
Custom import roots: Set import_root in apx.yaml to use a vanity domain instead of the Git hosting path. For example, with import_root: go.myorg.dev/apis, the Go module becomes go.myorg.dev/apis/proto/payments/ledger and the import becomes go.myorg.dev/apis/proto/payments/ledger/v1. See Configuration Reference for details.
Understanding the Configuration¶
The apx.yaml generated by apx init app contains your API's identity:
version: 1
org: <org>
repo: <app-repo>
# import_root: go.<org>.dev/apis # optional: custom Go import prefix
module_roots:
- internal/apis/proto
api: # canonical API identity
id: proto/payments/ledger/v1 # <format>/<domain>/<name>/<line>
format: proto
domain: payments
name: ledger
line: v1
lifecycle: beta # experimental -> beta -> stable -> deprecated -> sunset
source: # where the canonical copy lives
repo: github.com/<org>/apis
path: proto/payments/ledger/v1
releases:
current: v1.0.0-beta.1 # latest SemVer tag for this line
languages: # per-language derived coordinates
go:
module: github.com/<org>/apis/proto/payments/ledger # go.mod module path
import: github.com/<org>/apis/proto/payments/ledger/v1 # Go import path
Tip
You only need to supply the API ID (api.id), source repo, and lifecycle. APX derives the remaining fields — format, domain, name, line, source.path, and all languages coordinates — automatically via apx init app or apx inspect identity.
How Identity Flows Through the Workflow¶
| Field | Where it shows up |
|---|---|
api.id | Git tag prefix (proto/payments/ledger/v1/v1.2.3), overlay directory name, apx search results |
api.lifecycle | SemVer guardrails — beta APIs may only release 0.x or pre-release versions |
source.repo + source.path | Target for apx release PRs; base path in the canonical repo |
languages.go.module | go.mod synthesised during apx gen go and overlay setup |
languages.go.import | The import path your application code uses — unchanged from local dev through production |
1. Author Your Schema¶
Edit your schema files:
Example Protocol Buffer¶
syntax = "proto3";
package <org>.payments.ledger.v1;
option go_package = "github.com/<org>/apis/proto/payments/ledger/v1";
service LedgerService {
rpc CreateEntry(CreateEntryRequest) returns (CreateEntryResponse);
rpc GetEntry(GetEntryRequest) returns (GetEntryResponse);
}
message LedgerEntry {
string id = 1;
string account_id = 2;
int64 amount = 3;
string currency = 4;
int64 created_at = 5;
}
message CreateEntryRequest {
string account_id = 1;
int64 amount = 2;
string currency = 3;
}
message CreateEntryResponse {
LedgerEntry entry = 1;
}
message GetEntryRequest {
string id = 1;
}
message GetEntryResponse {
LedgerEntry entry = 1;
}
Note
No local go.mod required for authoring. Buf ignores it. apx release synthesizes the correct go.mod in the PR to canonical repo.
2. Local Development with Canonical Import Paths¶
Validate and test your schemas locally using canonical import paths:
# Download pinned toolchain
apx fetch
# Validate schema
apx lint # buf lint + other format linters
# Check for breaking changes (if you have a baseline)
apx breaking --against=HEAD^ # buf breaking / oasdiff / avro compat
# Generate code with canonical import paths (never committed)
apx gen go # writes to internal/gen/go/<api>@<ver>/ with canonical imports
apx sync # updates go.work to overlay canonical paths to local stubs
apx gen python # writes to internal/gen/python/<api>@<ver>/
# Test your code - imports use canonical paths, resolved via go.work
go test ./... # your code imports github.com/<org>/apis/proto/..., resolves to local stubs
App repo layout with canonical imports:
your-app/
├── go.mod # your app's module
├── go.work # managed by apx - overlays canonical -> local
├── internal/
│ ├── gen/
│ │ └── go/proto/<domain>/<api>@v1.2.3/
│ │ ├── go.mod # module github.com/<org>/apis/proto/<domain>/<api>
│ │ └── v1/*.pb.go # imports canonical path above
│ └── apis/... # your proto sources
└── main.go # imports github.com/<org>/apis/proto/<domain>/<api>/v1
Concrete example:
payment-service/
├── go.mod # module github.com/mycompany/payment-service
├── go.work # managed by apx
├── internal/
│ ├── gen/
│ │ ├── go/proto/payments/ledger@v1.2.3/
│ │ │ ├── go.mod # module github.com/myorg/apis/proto/payments/ledger
│ │ │ └── v1/*.pb.go # package ledgerv1
│ │ └── go/proto/users/profile@v1.0.1/
│ │ ├── go.mod # module github.com/myorg/apis/proto/users/profile
│ │ └── v1/*.pb.go # package profilev1
│ └── apis/... # your proto sources
└── main.go # imports github.com/myorg/apis/proto/payments/ledger/v1
go.work overlay:
go 1.22
use .
# Pattern: use ./internal/gen/go/proto/<domain>/<api>@v1.2.3
use ./internal/gen/go/proto/payments/ledger@v1.2.3
use ./internal/gen/go/proto/users/profile@v1.0.1
# apx sync adds one "use" per pinned API during local development
Application code using canonical imports:
// main.go - your application imports canonical paths
package main
import (
"context"
// Pattern: github.com/<org>/apis/proto/<domain>/<api>/v1
ledgerv1 "github.com/myorg/apis/proto/payments/ledger/v1"
usersv1 "github.com/myorg/apis/proto/users/profile/v1"
"google.golang.org/grpc"
)
func main() {
conn, _ := grpc.Dial("localhost:9090", grpc.WithInsecure())
defer conn.Close()
// Use generated clients from canonical imports
ledgerClient := ledgerv1.NewLedgerServiceClient(conn)
usersClient := usersv1.NewProfileServiceClient(conn)
// All imports resolve to local overlays during development
ledgerResp, err := ledgerClient.CreateEntry(context.Background(), &ledgerv1.CreateEntryRequest{
AccountId: "account-123",
Amount: 1000,
Currency: "USD",
})
userResp, err := usersClient.GetProfile(context.Background(), &usersv1.GetProfileRequest{
UserId: "user-456",
})
// ... handle responses
}
Important
Policy: /internal/gen/** is git-ignored. Never commit generated code. Commit apx.lock instead. Generated Go code uses canonical import paths resolved via go.work overlays.
3. Release Workflow¶
When ready to release your schema:
Validate Locally¶
Release via PR¶
The simplest path for teams: apx release prepare copies your module into the canonical repo on a feature branch, and apx release submit opens a pull request via the gh CLI.
# One-time: gh auth login
apx release prepare proto/payments/ledger/v1 \
--version v1.0.0-beta.1 \
--lifecycle beta \
&& apx release submit
APX will: 1. Shallow-clone the canonical repo 2. Copy your module files into proto/payments/ledger/v1/ 3. Generate go.mod if missing 4. Push a feature branch apx/release/proto-payments-ledger-v1/v1.0.0-beta.1 5. Open a PR on the canonical repo
Canonical Repo CI¶
On PR merge, canonical CI: - Re-validates schema - Verifies SemVer - Creates subdirectory tag (proto/payments/ledger/v1/v1.0.0-beta.1) - Go modules work automatically (Go proxy picks up the tag) - Other language packages (Maven, wheels, OCI) require optional CI plugins
4. Consuming APIs¶
Other teams can now discover and use your released API with seamless local-to-released transitions:
Discover APIs¶
Add Dependencies¶
# Add a specific version
apx add proto/payments/ledger/v1@v1.2.3
# This pins in apx.lock and records codegen preferences
Generate Client Code with Canonical Imports¶
apx gen go # -> internal/gen/go/<api>@<ver>/ with canonical import paths
apx sync # updates go.work to overlay canonical -> local stubs
apx gen python # -> internal/gen/python/<api>@<ver>/...
Your application code imports canonical paths:
// service.go - consuming the payments ledger API
package service
import (
"context"
// Canonical import - resolved to local overlay during development
ledgerv1 "github.com/myorg/apis/proto/payments/ledger/v1"
usersv1 "github.com/myorg/apis/proto/users/profile/v1"
)
type PaymentService struct {
ledgerClient ledgerv1.LedgerServiceClient
usersClient usersv1.ProfileServiceClient
}
func (s *PaymentService) ProcessPayment(ctx context.Context, userID, amount string) error {
// Use generated types and clients from canonical imports
user, err := s.usersClient.GetProfile(ctx, &usersv1.GetProfileRequest{
UserId: userID,
})
if err != nil {
return err
}
_, err = s.ledgerClient.CreateEntry(ctx, &ledgerv1.CreateEntryRequest{
AccountId: user.Profile.AccountId,
Amount: parseInt64(amount),
Currency: "USD",
})
return err
}
During development, Go resolves these imports via go.work overlay to local generated stubs.
Update Dependencies¶
# Update to latest compatible version (minor/patch)
apx update proto/payments/ledger/v1
# Upgrade to a new major API line
apx upgrade proto/payments/ledger/v1 --to v2
Switch to Released Module (No Import Changes!)¶
# Once the canonical module is released, seamlessly switch:
apx unlink proto/payments/ledger/v1 # remove go.work overlay
go get github.com/myorg/apis/proto/payments/ledger@v1.2.3
# Your application code remains completely unchanged:
# import ledgerv1 "github.com/myorg/apis/proto/payments/ledger/v1"
#
# Before: resolved to ./internal/gen/go/proto/payments/ledger@v1.2.3 via go.work
# After: resolved to released module github.com/myorg/apis/proto/payments/ledger@v1.2.3
#
# No find/replace, no import rewrites, no replace directives needed!
Example transition:
# go.work (before unlink)
go 1.22
use .
-use ./internal/gen/go/proto/payments/ledger@v1.2.3
-use ./internal/gen/go/proto/users/profile@v1.0.1
# go.mod (after go get)
module github.com/mycompany/payment-service
go 1.22
require (
+ github.com/myorg/apis/proto/payments/ledger v1.2.3
+ github.com/myorg/apis/proto/users/profile v1.0.1
)
// service.go - application code UNCHANGED
import (
ledgerv1 "github.com/myorg/apis/proto/payments/ledger/v1" // same import!
usersv1 "github.com/myorg/apis/proto/users/profile/v1" // same import!
)
CI/CD Patterns¶
App Repo CI (Release on Tag)¶
name: Release API from App Repo
on:
push:
tags: ['proto/*/*/v*/v*'] # e.g., proto/payments/ledger/v1/v1.2.3
jobs:
release:
runs-on: ubuntu-latest
permissions: { contents: read, pull-requests: write }
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- run: apx fetch
- run: apx lint && apx breaking --against=HEAD^
# Extract API ID and version from the tag
# Tag format: proto/payments/ledger/v1/v1.2.3
- run: |
TAG="${GITHUB_REF_NAME}"
API_ID="${TAG%/v*}" # proto/payments/ledger/v1
VERSION="${TAG##*/}" # v1.2.3
apx release prepare "$API_ID" --version "$VERSION" && apx release submit
Canonical Repo CI (Validate & Release)¶
name: Validate + Release API Modules
on:
pull_request:
paths: ['proto/**', 'openapi/**']
push:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: apx fetch
- run: apx lint && apx breaking --against=origin/main
# NOTE: For automated tag creation and package releasing,
# use `apx release prepare` + `apx release submit` + `apx release finalize`
# See the release docs for the full release pipeline.
Troubleshooting¶
Schema validation fails¶
- Check the specific error messages from
apx lint - For proto files, ensure buf.yaml configuration is correct
- Verify schema syntax matches the expected format
Code generation fails¶
- Ensure target language tools are installed (protoc, etc.)
- Check
buf.gen.yamlconfiguration for proto files - Verify output directories have write permissions
Interactive mode in CI / non-TTY environments¶
APX automatically detects non-interactive environments (CI, piped input, etc.) and disables prompts. In those environments, use explicit flags:
What's Next?¶
- Explore the Initialization Guide for detailed
apx initoptions and smart defaults - Learn about Local Development workflows for day-to-day use
- Review the Releasing Overview for the full release pipeline
- See the CLI Reference for all commands
Questions? Check the Troubleshooting FAQ or open a discussion.