Node.js (`nodejs`)
Generates a TypeScript service on Express 5, Fastify 5 or plain node:http. The models, the service interfaces and the routes do not depend on the framework; only a small adapter does, so you can switch the framework later and your code still compiles.
Ports, middleware and security run below the framework, on the node:http request: CORS, limits, rate limits,
security headers and JWT / OIDC behave the same on all three frameworks and the same as in the other generators.
Tokens are verified with node:crypto, with no JWT library.
Quick start
gnx create orders --type nodejs --dir services/orders --framework fastify --contract services/orders/api/orders.openapi.yaml --port 8080# services/orders/.genexy/component.yaml
type: nodejs
framework: fastify
nodeVersion: '22'
exposes:
- name: api
port: 8080
contracts:
- $ref: api/orders.openapi.yaml
security: auth
modules:
- name: auth
type: security
provider: oidc
issuer: https://sso.example.com/realms/shop
audience: orders-apiComponent spec
| Option | gnx create flag | Default | Notes |
|---|---|---|---|
framework | --framework | express | express, fastify or http (plain node:http) |
nodeVersion | --node | 22 | Node.js major version, 18 or newer |
packageName | the id | npm package name | |
basePath | from the contract | Path prefix of the API; otherwise taken from servers[0].url | |
exposes | --port, --contract | port 8080 | Ports: see Ports, Middleware & Security |
exposes[].middleware, .validation, .security | Middleware, validation, authentication | ||
modules | Security modules (type: security); databases and clients are not generated yet |
Contracts: OpenAPI 3.0 and 3.1. OpenAPI support: allOf (intersection), oneOf and anyOf (unions),
enums, const, nullable, additionalProperties, shared parameters, request bodies and responses; one service per
first tag.
What is generated
orders/
├─ .genexy/component.yaml the component spec (and generated.json)
├─ package.json, tsconfig.json yours (created once)
├─ Dockerfile, .env.example, README.md yours
├─ src/
│ ├─ main.ts stub: starts every port
│ ├─ services/OrdersServiceImpl.ts stub, one per OpenAPI tag
│ └─ gen/ generated, rewritten as a whole on every run
│ ├─ models.ts the schemas as TypeScript types
│ ├─ services/ <Tag>Service interfaces and the input type of each operation
│ ├─ routes/ framework-neutral operations with their contract and security
│ ├─ http.ts the framework adapter, HttpError and problem details
│ ├─ platform.ts middleware of the ports, token verification, Principal
│ ├─ settings.ts the ports and security modules of the spec
│ ├─ shapes.ts the properties of the schemas (unknown properties)
│ └─ app.ts createApp() of a port and startServer() of all ports
└─ test/app.test.ts stub: node:testWriting the business logic
Each service method gets an input object (path, query, header and cookie parameters and the body) and the
RequestContext with the framework request, the response and the caller:
import type * as M from '../gen/models';
import { HttpError, type RequestContext } from '../gen/http';
import type { GetOrderInput, OrdersService } from '../gen/services/OrdersService';
export class OrdersServiceImpl implements OrdersService {
async getOrder(input: GetOrderInput, ctx: RequestContext): Promise<M.Order> {
const order = await this.orders.find(input.orderId);
if (!order) throw HttpError.notFound('order ' + input.orderId + ' not found');
if (ctx.principal && !ctx.principal.hasRole('admin') && order.owner !== ctx.principal.subject) {
throw HttpError.forbidden('not your order');
}
return order;
}
}- Errors:
HttpError(badRequest,notFound,conflict,forbidden,new HttpError(status, message)) is answered as a problem detail withcodeandmessage;new HttpError(status, message, body)sends the error model of the contract exactly. Other errors answer 500 without details and are logged. - The caller:
ctx.principal, orcurrentPrincipal()fromsrc/gen/platformanywhere in the call. - Validation: required parameters are checked and parameters are converted to their types; unknown body properties are rejected when the port asks for it.
Request bodies are not validated against the rest of the schema yet (types, lengths, formats). Check them in your code where it matters.
Ports, middleware and security
startServer() starts every port of the spec: a server of the chosen framework that serves only the operations of
the port’s contracts, wrapped in the port middleware (withPolicy in platform.ts). /health (200) and
/healthz (204) answer on every port. All options:
Ports, Middleware & Security.
import { startServer } from '../src/gen/app';
const app = await startServer({
ports: { api: 0 }, // 0 = a free port
host: '127.0.0.1',
security: { auth: { jwksUrl: testJwksUrl } }, // override a security module in tests
portSettings: { api: { rateLimit: { enabled: false } } }
});
const res = await fetch(app.ports.url('api') + '/api/v1/orders/1');
await app.close();settings.ts holds the values of the spec; ${VAR} and ${VAR:default} in them read environment variables, and
GENEXY_PORTS_<NAME>_PORT (or PORT with one port) sets a port number.
Switching the framework
Change framework in the component spec and regenerate. package.json is yours and is not rewritten, so the log says
which dependencies the new framework needs; add them and run npm install.
Build and run
Needs Node.js. In the component folder:
npm install
npm run typecheck
npm test
npm run devNot generated yet
- Validation of request bodies against the schema (only unknown properties are checked)
- Databases, REST clients, Redis and messaging modules
apiKeyandbasicsecurity schemes, Keycloak incompose.yaml