Skip to Content
Genexy IDE Beta 0.9.1 · Download the Beta build
Genexy FeaturesCode GenerationPorts, Middleware & Security
Features · Code generation

Ports, Middleware & Security

PROIDEExperimental

All backend generators, Go, Rust, Spring Boot, ASP.NET Core, Node.js and NestJS, read the same settings from the component spec and behave the same way at runtime. The same requests get the same status codes, headers and error bodies from all of them. The same probe tests check every generator against the same expectations. This page describes these shared settings. Each generator page lists what is specific to its language.

Differences between the generators are listed in Differences at the end of this page.

A complete example

# services/orders/.genexy/component.yaml type: go-gnx-di # or axum, spring-boot, aspnet-core, nodejs, nestjs exposes: - name: public # key of the port in the configuration protocol: http port: 8080 contracts: - $ref: api/orders.openapi.yaml security: auth # the security module that checks tokens on this port middleware: cors: { allowedOrigins: ['https://shop.example.com'], allowCredentials: true, maxAge: 10m } rateLimit: { limit: 100, window: 1m, key: ip } maxRequestBytes: 1MiB requestTimeout: 30s securityHeaders: { hsts: true, contentSecurityPolicy: "default-src 'none'" } compression: true accessLog: true validation: unknownProperties: reject - name: admin port: 8081 contracts: - $ref: api/admin.openapi.yaml security: none # reachable only inside the cluster, no tokens modules: - name: auth type: security provider: oidc issuer: https://sso.example.com/realms/shop audience: orders-api

Ports

Every entry of exposes is a port with its own web server. A port serves the operations of its contracts and nothing else: an admin operation never answers on the public port. The same contract can be served on several ports, and a port can serve several contracts. If two contracts on one port use the same path and method, generation stops with an error.

KeyDefaultNotes
nameapi, then port2, …Kebab case. It names the port in the configuration and in tests
portthe next free one from 8080Two ports with the same number are an error
protocolhttpOnly HTTP is generated
contractsOpenAPI contracts: { $ref: <file> } relative to the component, or '#/contracts/<Name>'
securitythe only security moduleName of a security module, or none
middlewaresee below
validationsee below

Tests start every port on a free number and ask the application for its address. The generator pages show how in each language.

Middleware

The values in middleware are the defaults of the port. The generator writes them into the generated configuration file, so you can still change them per environment without regenerating: in your own configuration file or with environment variables (see Configuration). A value the generator does not understand stops the generation with the reason. A wrong value in the configuration stops the application at startup.

KeyDefaultBehaviour
corshttp://localhost:* and 127.0.0.1:*allowedOrigins, allowedMethods, allowedHeaders, exposedHeaders, allowCredentials, maxAge. * matches inside one part of an origin. cors: false turns CORS off. * together with allowCredentials is refused
rateLimitoffToken bucket per client: limit requests per window (at least 1s), burst, key: ip or key: header:<Name>. true means 100 per minute by IP. Answers 429 with Retry-After; every response has RateLimit-Limit and RateLimit-Remaining
maxRequestBytes10MiBLarger bodies get 413. Accepts 1MiB, 512KB or a number of bytes
requestTimeout60sThe deadline of the service call; 503 when it passes (not in Spring Boot, see its page)
securityHeadersonX-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer. Options: hsts, hstsMaxAge, frameOptions (DENY, SAMEORIGIN, none), referrerPolicy, contentSecurityPolicy. false turns them off
compressionfalsegzip for responses (not for images, media and archives)
accessLogfalseOne log line per request: method, path, status and duration

Every request also gets an X-Request-ID: the one the caller sent, or a new one. It is returned in the response and written into the logs; all generators but Spring Boot also put it into the problem details (requestId), and the generated REST clients forward it.

/healthz answers 204 on every port whose contract does not define it (all generators but Spring Boot), and is never rate limited or authenticated.

The rate limit is kept in memory, so each instance of the service counts on its own. A shared store (Redis) and settings for single operations (x-genexy-*) are planned.

Validation

The generated code validates every request against the contract before your code runs, so the service methods receive only valid input. The validation covers types, required properties and null, formats (date, date-time, uuid, email, uri…), enum, const, string lengths, pattern, number ranges, array sizes, nested objects, array items and map values. All violations of a request are returned in one 400 response:

{ "type": "about:blank", "title": "Bad Request", "status": 400, "detail": "Invalid request", "instance": "/api/v1/orders", "requestId": "4f1c…", "errors": [ { "field": "items[0].quantity", "message": "must be at least 1" }, { "field": "bogus", "message": "is not a known property" } ] }
KeyValuesNotes
unknownPropertiesignore (default), rejectreject answers 400 for properties the contract does not define, also in nested objects. Objects with additionalProperties accept anything

coerceParameters, responses, errorFormat, maxErrors and formats are planned. Today they produce a warning.

Authentication (JWT / OIDC)

A module of type security verifies the bearer tokens of the ports that use it. The contract decides what each operation needs. You do not write any security code, and no generator adds a security framework. The tokens are verified with the cryptography of the language (Go and Java standard library, System.Security.Cryptography, ring in Rust, node:crypto), so the behaviour is the same everywhere.

The security module

modules: # Keycloak or another OpenID Connect provider: the keys come from its discovery document - name: auth type: security provider: oidc issuer: https://sso.example.com/realms/shop audience: orders-api # A JWKS URL without discovery - name: partners type: security provider: jwt issuer: https://partners.example.com jwksUrl: https://partners.example.com/.well-known/jwks.json # A shared secret (HS256); keep the value out of the repository - name: internal type: security provider: jwt secret: ${INTERNAL_JWT_SECRET}
KeyDefaultNotes
provideroidc with only an issuer, else jwtoidc reads the key URL from <issuer>/.well-known/openid-configuration
issuerRequired for oidc. The iss claim must match
jwksUrljwt: keys for RS256/384/512, PS256/384/512 and ES256/384/512
secretjwt: key for HS256/384/512, at least 32 characters. Write it as ${ENV_VAR}, otherwise you get a warning
audienceOne value or a list. The aud claim, or Keycloak’s azp, must contain one of them
rolesClaimrealm_access.roles,rolesClaims with the roles. Use dots for nested claims and commas for several claims
scopesClaimscope,scpClaims with the scopes. A space-separated string or an array
usernameClaimpreferred_username
leeway1mAllowed clock difference for exp and nbf

A token is accepted when its signature is valid and its exp has not passed. exp is required. If set, nbf, iss and aud must also match. A module with keys accepts only signed algorithms with keys, and a module with a secret accepts only HS algorithms. A token with alg: none or with an algorithm of the other kind is refused. The keys are cached for an hour. An unknown key id reloads them at most every 30 seconds (key rotation). When the provider cannot be reached, the keys already loaded keep working.

exposes[].security picks the module of a port. Without it, a component with one security module uses that module on every port. With several modules every port must name one, or none.

What an operation requires

The contract describes it, the generated code enforces it:

security: # default of every operation of the contract - bearer: [] paths: /orders: get: security: - keycloak: [orders:read] # scopes the token must have post: security: # alternatives: one of them is enough - keycloak: [orders:write] - keycloak: [orders:admin] /orders/{id}: delete: x-genexy-roles: [admin] # roles the caller must have (all of them) /catalog: get: security: [] # public: no token needed components: securitySchemes: bearer: { type: http, scheme: bearer, bearerFormat: JWT } keycloak: type: oauth2 flows: { clientCredentials: { tokenUrl: https://sso.example.com/…/token, scopes: { orders:read: '', orders:write: '', orders:admin: '' } } }
  • Schemes of type http with scheme: bearer, oauth2 and openIdConnect are checked with the bearer token.
  • security: [] or an empty requirement {} makes an operation public. A token sent anyway is still verified, and your code sees its caller.
  • On a port with a security module, an operation that declares nothing requires a valid token.
  • apiKey and basic schemes are not checked yet. The generator warns and leaves them out.
  • A port without a security module gets the warning “security requirements are not enforced” when its contracts require tokens.
SituationResponse
No token on an operation that requires one401, WWW-Authenticate: Bearer
Invalid, expired or foreign token401, WWW-Authenticate: Bearer error="invalid_token", error_description=…
Authorization that is not Bearer <token>401, error="invalid_request"
Valid token without the scopes403, WWW-Authenticate: Bearer error="insufficient_scope", scope=…
Valid token without a role of x-genexy-roles403
The keys cannot be loaded503

All of them are problem details. Unknown paths still answer 404.

The caller in your code

GeneratorAPI
Goauth.FromContext(ctx) returns *auth.Principal, or nil for an anonymous request
Rustplatform::auth::principal() returns Option<Arc<Principal>>
Spring BootCurrentPrincipal.get() returns Optional<Principal> in Java, Principal? in Kotlin
ASP.NET CoreRequestContext.Principal returns a Principal, or null for an anonymous request
Node.jsctx.principal of the service method, or currentPrincipal() from src/gen/platform
NestJScurrentPrincipal() from src/generated/platform

A principal has the subject (sub), the username, the roles, the scopes, all claims, and HasRole / HasScope. Your code never sees a JWT library or a framework type.

Errors on the wire

Every error is an RFC 9457 problem detail (application/problem+json) with type, title, status, detail and instance (all generators but Spring Boot add requestId), with the same status codes in every generator.

SituationStatus
Invalid request (with errors)400
Missing or invalid token / missing scope or role401 / 403
Unknown path, wrong method, wrong media type404 / 405 / 415
Body too large, too many requests413 / 429
An HTTP error from your codeits status, with code and message
An HTTP error with the contract’s error modelits status, the body exactly as you return it
A database constraint violation / a missing row409 (with constraint) / 404
A failed call through a generated REST client502
A method you have not implemented yet501
Request timeout503
Any other error500, without details (logged with the request id)

Configuration

The generator writes the values of the spec into a generated configuration file that you do not edit. Put your changes into your own file or into environment variables. They win over the generated file.

GeneratorGenerated defaultsYoursEnvironment variable of a port number
Gointernal/bootstrap/application.yamlconfig/application.yaml, .envGENEXY_PORTS_API_PORT
Rustsrc/bootstrap/application.yamlconfig/application.yaml, .envGENEXY_PORTS_API_PORT
Spring Bootapp/src/main/resources/application.ymlapp/src/main/resources/config/application.ymlGENEXY_PORTS_API_PORT
ASP.NET Coreapp/genexy.jsonapp/appsettings.jsonGENEXY__PORTS__API__PORT
Node.jssrc/gen/settings.tsstartServer({ portSettings, security }), envGENEXY_PORTS_API_PORT
NestJSsrc/generated/settings.tsstartPorts(app, { portSettings, security }), envGENEXY_PORTS_API_PORT
Settings ofGo, Rust and Spring BootASP.NET Core
A port and its middlewaregenexy.ports.<port>.*Genexy:Ports:<port>:*
The security module of a portgenexy.ports.<port>.securityGenexy:Ports:<port>:Security
A security modulegenexy.security.<name>.*Genexy:Security:<name>:*
A databasegenexy.databases.<name>.*Genexy:Databases:<name>:*

Values can read environment variables with ${NAME} or ${NAME:default}, for example issuer: ${AUTH_ISSUER:http://localhost:8180/realms/shop}. In every generator but Spring Boot, a component with only one port also takes its number from the PORT variable (container platforms). Node.js and NestJS keep the settings in TypeScript: settings.ts holds the values of the spec, the startServer / startPorts options override them per port and per module, and GENEXY_PORTS_<NAME>_PORT sets a port number.

Differences

GeneratorDifference
Spring BootrequestTimeout is not enforced (synchronous Spring MVC); unknownProperties: reject applies to the whole component; the body limit uses Content-Length; no /healthz; no requestId in problem details
Node.jsRequest bodies are checked only for unknown properties, not against the rest of the schema
NestJSThe DTOs do not check string lengths, patterns or number ranges yet