Local Development¶
This page covers the day-to-day development workflow in an app repository — authoring schemas, validating them, generating code with canonical import paths, and testing locally before releasing.
Prerequisites¶
- APX CLI installed (Installation)
- App repo initialized with
apx init app(Quickstart) - Buf CLI available (APX installs it automatically via
apx fetch) - Go 1.22+ (for Go code generation and
go.workoverlays)
The Development Loop¶
# 1. Author or edit schemas
vim internal/apis/proto/payments/ledger/v1/ledger.proto
# 2. Validate
apx lint
apx breaking --against HEAD^
# 3. Generate code with canonical imports
apx gen go
# 4. Sync go.work overlays
apx sync
# 5. Build and test — imports use canonical paths
go test ./...
# 6. Repeat
Authoring Schemas¶
Schema files live under internal/apis/ in the app repo, organized by format, domain, name, and version line:
internal/apis/proto/payments/ledger/v1/ledger.proto
internal/apis/proto/payments/ledger/v1/types.proto
internal/apis/openapi/billing/invoices/v1/invoices.yaml
Setting go_package¶
For protobuf schemas, set go_package to the canonical import path — not your app repo path:
syntax = "proto3";
package acme.payments.ledger.v1;
// Points to the canonical repo, not the app repo
option go_package = "github.com/acme-corp/apis/proto/payments/ledger/v1";
service LedgerService {
rpc CreateEntry(CreateEntryRequest) returns (CreateEntryResponse);
}
APX validates this during apx lint and apx release prepare, warning if the go_package doesn't match the canonical path derived from the API ID.
Note
No local go.mod is needed for the schema directory. Buf ignores go.mod. APX synthesizes the correct go.mod when releasing to the canonical repo.
Validation¶
Lint¶
Run format-specific linting on all schemas:
For protobuf, this runs buf lint against your buf.yaml configuration. For OpenAPI, it runs OpenAPI-specific validators.
Breaking Change Detection¶
Compare your working tree against a baseline to detect backward-incompatible changes:
# Against the previous commit
apx breaking --against HEAD^
# Against the main branch
apx breaking --against origin/main
# Against a specific tag
apx breaking --against proto/payments/ledger/v1/v1.0.0
For protobuf, this runs buf breaking. It checks for field removals, type changes, renumbering, and other wire-incompatible changes.
SemVer Suggestion¶
APX can suggest the appropriate version bump based on detected changes:
Code Generation¶
Generate language-specific code from your schemas:
# Generate Go code
apx gen go
# Generate Python code
apx gen python
# Generate Java code
apx gen java
# Clean output before regenerating
apx gen go --clean
Output Structure¶
Generated code is written to internal/gen/<lang>/ with canonical module structure:
internal/gen/
├── go/
│ └── proto/payments/ledger@v1.2.3/
│ ├── go.mod # module github.com/acme-corp/apis/proto/payments/ledger
│ └── v1/
│ ├── ledger.pb.go # package ledgerv1
│ └── ledger_grpc.pb.go # gRPC stubs
└── python/
└── proto/payments/ledger/v1/
├── pyproject.toml # name = "acme-payments-ledger-v1"
└── acme_apis/payments/ledger/v1/
└── __init__.py # leaf for generated code
The generated go.mod uses the canonical module path (github.com/<org>/apis/proto/...), which is the key to the overlay system.
Important
Never commit generated code. The internal/gen/ directory should be in .gitignore. Commit apx.lock instead — it ensures reproducible generation.
go.work Overlays¶
The overlay system is what makes canonical import paths work during local development.
How It Works¶
apx gen gogenerates code intointernal/gen/go/<api>@<version>/with ago.modthat declares the canonical module pathapx syncadds ausedirective ingo.workpointing to each generated directory- When you run
go buildorgo test, Go resolves canonical import paths to the local generated code via thego.workoverlay
# go.work (managed by apx sync)
go 1.22
use .
use ./internal/gen/go/proto/payments/ledger@v1.2.3
use ./internal/gen/go/proto/users/profile@v1.0.1
Syncing¶
After generating code, sync the go.work file:
This scans internal/gen/go/ for all overlay directories and updates go.work accordingly.
Using Canonical Imports¶
Your application code imports canonical paths as if using the released module:
package main
import (
"context"
ledgerv1 "github.com/acme-corp/apis/proto/payments/ledger/v1"
usersv1 "github.com/acme-corp/apis/proto/users/profile/v1"
)
func main() {
// These imports resolve to ./internal/gen/go/... via go.work
client := ledgerv1.NewLedgerServiceClient(conn)
resp, err := client.CreateEntry(context.Background(), &ledgerv1.CreateEntryRequest{
AccountId: "acct-123",
Amount: 1000,
Currency: "USD",
})
}
During local development, Go resolves these imports via go.work to the local overlay. After releasing, you can switch to the real released module with no import changes.
Python Development Loop¶
For Python consumers, the workflow parallels Go but uses editable installs instead of go.work:
# 1. Generate Python packages
apx gen python
# 2. Link into your virtualenv (editable install)
source .venv/bin/activate
apx link python
# 3. Import generated code in your Python app
# from acme_apis.payments.ledger.v1 import ledger_pb2
# 4. Test
pytest
# 5. When ready for released packages
apx unlink proto/payments/ledger/v1
pip install acme-payments-ledger-v1==1.2.3
Key differences from Go:
- No
go.workequivalent — Python usespip install -e(editable installs) for local resolution - Namespace packages — all overlays share the
<org>_apisnamespace viapkgutil.extend_path - Code is always generated locally — never pulled from PyPI. You control your own grpc/protobuf versions.
Java Development Loop¶
For Java consumers, APX publishes schema artifacts to Maven. Consumers generate Java code locally via Maven's generate-sources phase:
# 1. Add dependency
apx add proto/payments/ledger/v1@v1.2.3
# 2. Add Maven dependency to pom.xml
# com.acme.apis:payments-ledger-v1-proto:1.2.3
# 3. Run Maven build (generates Java in target/generated-sources/)
mvn compile
For local development before schemas are released:
# Install schema artifacts to ~/.m2 (planned)
apx link java
# Maven resolves from local cache
mvn compile
# When ready for released artifacts
apx unlink proto/payments/ledger/v1
# Update pom.xml with released version
Key differences from Go and Python:
- No APX code generation -- Maven's protobuf plugin handles generation in
generate-sources - Maven-native resolution --
~/.m2for local dev, Maven Central/private registry for released artifacts - Schema artifacts -- APX publishes
.protofiles as jars, not generated Java stubs
TypeScript Development Loop¶
For TypeScript consumers, APX publishes schema packages to npm with scoped package names:
# 1. Add dependency
apx add proto/payments/ledger/v1@v1.2.3
# 2. Install npm package
npm install @acme/payments-ledger-v1-proto
# 3. Import in your TypeScript code
# import { LedgerService } from "@acme/payments-ledger-v1-proto";
# 4. Build and test
npm run build && npm test
For local development before schemas are released:
# Link schema packages locally (planned)
apx link typescript
# npm resolves from local link
npm run build
# When ready for released packages
apx unlink proto/payments/ledger/v1
npm install @acme/payments-ledger-v1-proto
Key differences from Go and Python:
- npm-native resolution --
npm linkfor local dev, npm registry for released packages - Scoped packages -- all packages use
@<org>/scope for namespace isolation -protosuffix -- distinguishes schema packages from application packages
Adding Dependencies¶
To use schemas released by other teams:
# Add a dependency at a specific version
apx add proto/payments/ledger/v1@v1.2.3
# Add without a version (uses latest)
apx add proto/users/profile/v1
# Generate code for the dependency
apx gen go
apx sync
Dependencies are recorded in apx.yaml and pinned in apx.lock.
See Adding Dependencies for details.
Switching to Released Modules¶
When the schema is released to the canonical repo and you're ready to consume the real module instead of the local overlay:
# Remove the overlay
apx unlink proto/payments/ledger/v1
# Add the released module to go.mod
go get github.com/acme-corp/apis/proto/payments/ledger@v1.2.3
Your application code stays exactly the same — the import path github.com/acme-corp/apis/proto/payments/ledger/v1 now resolves to the released module instead of the local overlay.
# go.work (after unlink)
go 1.22
use .
-use ./internal/gen/go/proto/payments/ledger@v1.2.3
# go.mod (after go get)
require (
+ github.com/acme-corp/apis/proto/payments/ledger v1.2.3
)
# main.go — UNCHANGED
import ledgerv1 "github.com/acme-corp/apis/proto/payments/ledger/v1"
Fetching Toolchains¶
APX manages toolchain versions (Buf, protoc plugins, etc.) via apx.lock:
Tools are cached in .apx-tools/ (also gitignored). This ensures everyone on the team uses identical tool versions.
Common Workflows¶
New Schema from Scratch¶
# Initialize app repo
apx init app --org acme-corp --repo payment-service internal/apis/proto/payments/ledger
# Author your schema
vim internal/apis/proto/payments/ledger/v1/ledger.proto
# Validate
apx lint
# Generate and test
apx gen go && apx sync
go test ./...
Iterating on an Existing Schema¶
# Edit schema
vim internal/apis/proto/payments/ledger/v1/ledger.proto
# Check for breaking changes
apx breaking --against HEAD^
# Suggest version bump
apx semver suggest --against HEAD^
# Regenerate and test
apx gen go --clean && apx sync
go test ./...
Consuming a Team's Released API¶
# Discover available APIs
apx search payments
# Inspect details
apx show proto/payments/ledger/v1
# Add as dependency
apx add proto/payments/ledger/v1@v1.2.3
# Generate client code
apx gen go && apx sync
# Use in your code with canonical imports
# import ledgerv1 "github.com/acme-corp/apis/proto/payments/ledger/v1"
Tips¶
- Run
apx syncafter everyapx gen go— the overlay won't work without thego.workentry - Use
apx gen go --cleanwhen switching versions to avoid stale generated files - Check
apx inspect identityto verify the canonical coordinates APX will use - Keep
internal/gen/in.gitignore— commitapx.lockfor reproducibility - Use
apx --json showto get machine-readable API metadata for scripts
Next Steps¶
- Release Workflow — release to the canonical repo
- CI Integration — automate validation and releasing
- Code Generation — multi-language generation details
- Versioning Strategy — SemVer and API line conventions