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

Node.js (`nodejs`)

PROIDEExperimental

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-api

Component spec

Optiongnx create flagDefaultNotes
framework--frameworkexpressexpress, fastify or http (plain node:http)
nodeVersion--node22Node.js major version, 18 or newer
packageNamethe idnpm package name
basePathfrom the contractPath prefix of the API; otherwise taken from servers[0].url
exposes--port, --contractport 8080Ports: see Ports, Middleware & Security
exposes[].middleware, .validation, .securityMiddleware, validation, authentication
modulesSecurity 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:test

Writing 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 with code and message; 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, or currentPrincipal() from src/gen/platform anywhere 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 dev

Not generated yet

  • Validation of request bodies against the schema (only unknown properties are checked)
  • Databases, REST clients, Redis and messaging modules
  • apiKey and basic security schemes, Keycloak in compose.yaml