Quick Start#
Get up and running with APXβs canonical repo pattern using canonical import paths in under 10 minutes! This guide covers the essential workflow for organization-wide API schema management.
Overview: Canonical Import Paths#
APX uses a canonical import path approach with two types of repos:
Canonical Repo (
github.com/<org>/apis) - Single source of truth for all organization APIsApp Repos - Where teams author schemas, generate stubs with canonical import paths, and release to canonical repo
Key Benefits:
One import path everywhere - no rewrites when switching from local to released
go.work overlays - seamless local development with canonical paths
No replace gymnastics - clean dependency management
Path Mapping Reference#
Every API in the canonical repo maps to a deterministic set of coordinates:
Coordinate |
Example |
|---|---|
API ID |
|
Source path |
|
Proto package |
|
Go module (v1) |
|
Go import (v1) |
|
Go module (v2) |
|
Go import (v2) |
|
Git tag |
|
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.
1. Bootstrap the Canonical API Repo#
First, create your organizationβs canonical API repository:
# Create github.com/<org>/apis (public or private)
# This is what consumers depend on
Initialize the Structure#
git clone https://github.com/<org>/apis.git
cd apis
# Create the canonical structure
apx init canonical --org=<org> --repo=apis
This creates:
apis/
βββ buf.yaml # org-wide lint/breaking policy
βββ buf.work.yaml # workspace aggregating version dirs
βββ CODEOWNERS # per-path ownership
βββ catalog/
β βββ catalog.yaml # generated index of APIs/owners/tags
βββ proto/ # (add openapi/avro/jsonschema/parquet as needed)
βββ payments/
βββ ledger/
βββ go.mod # v1 module: module github.com/<org>/apis/proto/payments/ledger
βββ v1/
β βββ ledger.proto # package <org>.payments.ledger.v1
βββ v2/ # (empty until you add v2)
Protection Settings#
Protect
mainbranch - require PR reviewsProtect tag patterns:
proto/**/v*,openapi/**/v*- only CI can create tags
4. 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
}
Generated code structure (local overlay):
internal/gen/go/proto/payments/ledger@v1.2.3/
βββ go.mod # module github.com/myorg/apis/proto/payments/ledger
βββ v1/
β βββ ledger.pb.go # package ledgerv1
β βββ ledger_grpc.pb.go # imports canonical path
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.
5. Release Workflow#
When ready to release your schema:
1. Validate Locally#
apx lint && apx breaking --against=HEAD^ && apx semver suggest --against=HEAD^
2. 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:
Shallow-clone the canonical repo
Copy your module files into
proto/payments/ledger/v1/Generate
go.modif missingPush a feature branch
apx/release/proto-payments-ledger-v1/v1.0.0-beta.1Open a PR on the canonical repo
3. 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
6. Consuming APIs with Canonical Import Paths#
Other teams can now discover and use your released API with seamless local-to-released transitions:
Discover APIs#
apx search payments # search the catalog
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!
)
Common 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 lintFor 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 filesVerify 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:
apx init app --org=myorg --repo=myapp --non-interactive internal/apis/proto/payments/ledger
Interactive mode works normally in a terminal with TTY support.
Whatβs Next?#
Learn about Interactive Initialization features
Explore the User Guide for advanced workflows
Check out Examples for specific use cases
Review the CLI Reference for all commands
Questions? Check the FAQ or open a discussion.