Go (`go-gnx-di`)
Generates a Go backend as a Go workspace (go.work). The main module holds your code. Every API contract, every
database and the runtime get a Go module of their own. The runtime covers dependency injection, configuration,
logging and the HTTP platform. Your code sees only the public pkg/ packages of these modules. The Go internal
rule and a generated architecture test keep it that way.
The HTTP layer is built on chi . PostgreSQL is accessed through
pgx or database/sql. Everything else is the standard library: tokens are verified
with stdlib crypto, without a JWT library. All versions are pinned by the generator, and go.sum is generated too, so
the same spec always builds the same way.
Quick start
gnx create orders --type go-gnx-di --dir services/orders --module github.com/acme/orders --contract services/orders/api/orders.openapi.yaml --port 8080or ask your AI for “a Go service from contracts/orders.openapi.yaml”. The component spec it writes:
# services/orders/.genexy/component.yaml
type: go-gnx-di
modulePath: github.com/acme/orders
goVersion: '1.25'
exposes:
- name: api
port: 8080
contracts:
- $ref: api/orders.openapi.yaml
modules:
- name: main-db
type: postgresql-adapter
driver: pgx
contract: { $ref: db/orders.yaml }Then implement the stubs in internal/app/ and run go run ..
Component spec
| Option | gnx create flag | Default | Notes |
|---|---|---|---|
modulePath | --module | example.com/<id> | Go module path of the main module; the modules are <modulePath>/modules/<name> |
goVersion | --go | 1.25 | 1.25 or newer: the go directive of every go.mod and the image of the Dockerfile |
name, description, version | the id, –, 1.0.0 | README and genexy.app.* | |
basePath | from the contract | Path prefix of every contract; otherwise taken from servers[0].url | |
fileNaming | pascal | pascal (OrdersServiceImpl.go) or snake (orders_service_impl.go) | |
banner | Text printed at startup | ||
dependencies | Extra modules of the main module as module@vX.Y.Z | ||
exposes | --port, --contract | one port, 8080 | Ports: see Ports, Middleware & Security |
exposes[].middleware, .validation, .security | Middleware, validation, authentication | ||
modules | Databases and security modules, see below |
Modules
| Type | Contract | What you get |
|---|---|---|
postgresql-adapter | sqlschema | modules/db-<name>: rows, repositories, queries, transactions, migrations. driver: pgx (default) or driver: sql (database/sql on pgx). With several databases, mark one primary: true |
security | JWT / OIDC bearer tokens for the ports that use it. See Authentication |
MySQL, REST clients, Redis and messaging modules are not generated for Go yet. The generator skips them with a warning.
What is generated
orders/
├─ .genexy/component.yaml the component spec (and generated.json: the files the generator owns)
├─ go.work, go.mod, go.sum generated
├─ main.go generated
├─ compose.yaml generated with a database: PostgreSQL for local runs and tests
├─ config/application.yaml yours: your settings
├─ .env.example, Dockerfile, README.md yours (created once)
├─ migrations/main-db/ yours: your own migrations of main-db
├─ internal/
│ ├─ bootstrap/ generated: wiring, application.yaml (defaults), architecture test
│ ├─ ports/ generated: one HTTP server per port, router and middleware
│ └─ app/ yours
│ ├─ app_test.go stub: starts the application on free ports
│ └─ orders/OrdersServiceImpl.go stub, one per OpenAPI tag
└─ modules/ generated, never edit
├─ api-orders/ pkg/model, pkg/service (<Tag>Service interfaces), internal controllers
├─ db-main-db/ pkg/model (rows), pkg/service (repositories…), migrations/V<n>__*.sql
└─ platform/, inject/, config/, logger/The public API of the runtime for your code is in modules/platform/pkg:
| Package | Use |
|---|---|
httperr | HTTP errors from your services |
auth | The caller of the request (auth.FromContext(ctx)) |
dberr | Database errors (ConflictError, NotFoundError) |
reqctx | The request id (reqctx.RequestID(ctx)) |
types | Date, LocalTime, Duration, Decimal, Nullable[T], UploadedFile |
lifecycle | Start and stop hooks |
Your code must not import chi, pgx, lib/pq, internal/bootstrap, internal/ports or the HTTP packages of the
platform (web, schema, server). internal/bootstrap/architecture_test.go fails when it does.
Writing the business logic
There is one <Tag>ServiceImpl per OpenAPI tag. A new stub answers every operation with 501, so the service builds
and runs right away. Every method gets the request’s context.Context, typed path parameters, a <Op>Params struct
with the query, header and cookie parameters, and the decoded body:
package orders
import (
"context"
"github.com/acme/orders/modules/api-orders/pkg/model"
api "github.com/acme/orders/modules/api-orders/pkg/service"
db "github.com/acme/orders/modules/db-main-db/pkg/service"
"github.com/acme/orders/modules/inject"
"github.com/acme/orders/modules/platform/pkg/auth"
"github.com/acme/orders/modules/platform/pkg/httperr"
)
type OrdersServiceImpl struct {
orders db.OrderRepository
items db.OrderItemRepository
tx db.MainDBTransactions
}
var _ api.OrdersService = (*OrdersServiceImpl)(nil)
// Construct is called once at startup: look up what the logic needs.
func (s *OrdersServiceImpl) Construct(inj *inject.Injector) {
s.orders = inject.InjectFrom[db.OrderRepository](inj)
s.items = inject.InjectFrom[db.OrderItemRepository](inj)
s.tx = inject.InjectFrom[db.MainDBTransactions](inj)
}
func (s *OrdersServiceImpl) GetOrder(ctx context.Context, orderID int64) (*model.Order, error) {
row, err := s.orders.FindByID(ctx, orderID)
if err != nil {
return nil, err
}
if row == nil {
return nil, httperr.NotFound("order not found")
}
if caller := auth.FromContext(ctx); caller != nil && !caller.HasRole("admin") && row.Owner != caller.Subject {
return nil, httperr.Forbidden("not your order")
}
return toAPI(row), nil
}- Dependencies: look up repositories, transactions and other services in
Constructwithinject.InjectFrom[T](inj). Your own packages and data files go into new folders underinternal/app/. - Errors: return
httperr.New(status, code, message)or one ofBadRequest,NotFound,Conflict,Forbidden,Unprocessable…;httperr.WithBody(status, message, body)sends the error model of the contract as it is. Any other error becomes a 500. All of them are problem details, see Errors on the wire. - New operations: after the contract gains an operation, the stub is not changed (it is yours). The generator
prints the exact signatures of the missing methods, and
go buildreports them.
Types: OpenAPI objects become structs with the properties in the order of the contract. uuid becomes
uuid.UUID, date-time time.Time and date types.Date. Optional properties are pointers. An optional property
that can also be null is types.Nullable[T], which tells missing, null and a value apart. Enums are typed string
or integer constants with Valid().
Databases
A postgresql-adapter module turns the SQL schema contract into
modules/db-<name>:
Interface (pkg/service) | Methods |
|---|---|
<Row>Repository | FindByID, ExistsByID, FindAll, FindPage, Count, FindBy<Column> (from foreign keys, indexes and unique constraints), Insert, Update, DeleteByID |
<Db>Queries | One method per named query of the contract |
<Db>Transactions | InTransaction(ctx, fn), ReadOnly(ctx, fn): repository calls with that ctx join the transaction |
// dbmodel is github.com/acme/orders/modules/db-main-db/pkg/model (the rows)
err := s.tx.InTransaction(ctx, func(ctx context.Context) error {
order, err := s.orders.Insert(ctx, row)
if err != nil {
return err // rolls back
}
_, err = s.items.Insert(ctx, dbmodel.OrderItem{OrderID: order.ID, ProductID: productID, Quantity: 1})
return err
})A constraint violation is a dberr.ConflictError (answered as 409 with the constraint name); Update of a missing
row is a dberr.NotFoundError. A column without nullable: false is nullable, as in SQL.
Migrations belong to the module: the first generation writes V1__init.sql, and every later change of the schema
writes the next V<n>__*.sql from the difference to schema.snapshot.json. Migrations already written are never
changed; dropping data gives a warning. At startup the application waits for the database and applies the migrations
under a lock (PostgreSQL advisory lock), recording them with a checksum in genexy_schema_history. A changed
applied migration stops the start. Your own migrations go into migrations/<name>/V<n>_<m>__*.sql and run in order
with the generated ones. The history is compatible with the Rust generator.
Settings: genexy.databases.<name>.url, username, password, schema, migrate, connectTimeout and
pool.*, or the environment variables <NAME>_URL, <NAME>_USERNAME, <NAME>_PASSWORD (MAIN_DB_URL). The
defaults point to the database of the generated compose.yaml.
Ports, middleware and security
Every port has its own http.Server and router, and there is one injector, so each service exists once for all
ports. The middleware runs in this order: request id, panic recovery (500), access log, security headers, CORS, body
limit, request timeout, authentication, rate limit, validation options, gzip. The authorization of each route checks
what its contract requires. All options are described in
Ports, Middleware & Security.
caller := auth.FromContext(ctx) // nil: anonymous (public operation or a port without security)
if caller != nil {
log.Infof("%s (%s) roles=%v scopes=%v", caller.Username, caller.Subject, caller.Roles, caller.Scopes)
}In tests, auth.WithPrincipal(ctx, &auth.Principal{Subject: "u1", Roles: []string{"admin"}}) calls a service as
that caller.
Configuration
The settings are read in layers, and a later layer wins:
internal/bootstrap/application.yaml(generated: the values of the spec)config/application.yaml(yours).env- environment variables:
genexy.ports.admin.portisGENEXY_PORTS_ADMIN_PORT bootstrap.WithConfig(key, value)in tests
${NAME:default} in a value reads an environment variable. Read your own settings in Construct with
config.Get[T](inject.InjectFrom[*config.Config](inj), "app").
Tests
| File | Owner | Checks |
|---|---|---|
internal/bootstrap/architecture_test.go | generated | internal/app/ imports only what it may |
internal/bootstrap/ports_test.go | generated | With several ports: every operation answers only on its own port |
internal/app/app_test.go | yours | Starts the application on free ports and calls /healthz |
Start the application in your own tests the same way:
a, err := bootstrap.New(bootstrap.WithPort("api", 0), bootstrap.WithConfig("genexy.security.auth.jwksUrl", jwks.URL))
if err != nil {
t.Fatal(err)
}
if err := a.Start(ctx); err != nil {
t.Fatal(err)
}
defer a.Stop(ctx)
url := a.Ports.URL("api")Tests with a database need PostgreSQL: docker compose up -d with the generated compose.yaml, and
GENEXY_TEST_DATABASES=external. Without it, bootstrap.DatabasesAvailable() tells the tests to skip.
Build and run
Needs Go 1.25 or newer. In the component folder:
go build ./...
go vet ./...
go test ./...
go run .go.mod and go.sum are exactly what go mod tidy would write, so go build needs no extra step. Debug the service
with Delve from a run configuration.
Not generated yet
- REST and WebSocket clients, MySQL, MariaDB, Oracle, MongoDB, Redis, Kafka and NATS modules
oneOf,anyOfand discriminators (generated asany), response headers and several success statuses,readOnly/writeOnly, streaming bodiesapiKeyandbasicsecurity schemes, Keycloak incompose.yaml- A generated Dockerfile (
docker.enabled) and Kubernetes manifests; today the Dockerfile is a starter you own