Skip to Content
Genexy IDE Beta 0.9.1 · Download the Beta build
Features · Code generation

Go (`go-gnx-di`)

PROIDEExperimental

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 8080

or 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

Optiongnx create flagDefaultNotes
modulePath--moduleexample.com/<id>Go module path of the main module; the modules are <modulePath>/modules/<name>
goVersion--go1.251.25 or newer: the go directive of every go.mod and the image of the Dockerfile
name, description, versionthe id, –, 1.0.0README and genexy.app.*
basePathfrom the contractPath prefix of every contract; otherwise taken from servers[0].url
fileNamingpascalpascal (OrdersServiceImpl.go) or snake (orders_service_impl.go)
bannerText printed at startup
dependenciesExtra modules of the main module as module@vX.Y.Z
exposes--port, --contractone port, 8080Ports: see Ports, Middleware & Security
exposes[].middleware, .validation, .securityMiddleware, validation, authentication
modulesDatabases and security modules, see below

Modules

TypeContractWhat you get
postgresql-adaptersqlschemamodules/db-<name>: rows, repositories, queries, transactions, migrations. driver: pgx (default) or driver: sql (database/sql on pgx). With several databases, mark one primary: true
securityJWT / 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:

PackageUse
httperrHTTP errors from your services
authThe caller of the request (auth.FromContext(ctx))
dberrDatabase errors (ConflictError, NotFoundError)
reqctxThe request id (reqctx.RequestID(ctx))
typesDate, LocalTime, Duration, Decimal, Nullable[T], UploadedFile
lifecycleStart 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 Construct with inject.InjectFrom[T](inj). Your own packages and data files go into new folders under internal/app/.
  • Errors: return httperr.New(status, code, message) or one of BadRequest, 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 build reports 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>RepositoryFindByID, ExistsByID, FindAll, FindPage, Count, FindBy<Column> (from foreign keys, indexes and unique constraints), Insert, Update, DeleteByID
<Db>QueriesOne method per named query of the contract
<Db>TransactionsInTransaction(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:

  1. internal/bootstrap/application.yaml (generated: the values of the spec)
  2. config/application.yaml (yours)
  3. .env
  4. environment variables: genexy.ports.admin.port is GENEXY_PORTS_ADMIN_PORT
  5. 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

FileOwnerChecks
internal/bootstrap/architecture_test.gogeneratedinternal/app/ imports only what it may
internal/bootstrap/ports_test.gogeneratedWith several ports: every operation answers only on its own port
internal/app/app_test.goyoursStarts 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, anyOf and discriminators (generated as any), response headers and several success statuses, readOnly / writeOnly, streaming bodies
  • apiKey and basic security schemes, Keycloak in compose.yaml
  • A generated Dockerfile (docker.enabled) and Kubernetes manifests; today the Dockerfile is a starter you own