# Nexus Client Code Generator - TypeScript SDK

> For the complete documentation index, see [llms.txt](https://docs.temporal.io/llms.txt).
> Any documentation page is available as raw Markdown by appending `.md` to its URL.

> How to install nexgen and generate typed Nexus models and Service definitions for TypeScript.

A [Nexus Service](/nexus/services) is a contract meant to be shared across team boundaries.
Those teams often work in different languages, so the same request and response types get hand-written for each SDK.
Hand-written copies drift: a field is required on one side and optional on the other, a bound is enforced by the caller but not the handler.

The **[Nexus Client Code Generator](https://github.com/temporalio/nexgen)** removes those copies.
You describe your types and [Nexus Operations](/nexus/operations) once in a definition file, and the generator emits the equivalent library code for Go, Java, Python, and TypeScript.
The generator is a command-line tool named `nexgen`, distributed from the [temporalio/nexgen](https://github.com/temporalio/nexgen) repository.

For a short overview of what the generator produces and why, see [Nexus Client Code Generator](/nexus/client-code-generator).
This page covers installation, schema authoring, and generating and using TypeScript output.

> **⚠️ Caution:**
>
> `nexgen` is pre-release software and may not retain backwards compatibility with previous versions of the tool.
> It is not yet published to any package registry, so you build it from source as described in [Install the generator](#install-the-generator).
>

## What the generator produces

The generator produces a client library in Go, Java, Python, or TypeScript for the inputs and outputs of your Nexus Operations. The generated types check values against the contract as they are sent and received, so a violation surfaces as an error rather than as bad data.

That client library contains three things:

- **A typed model.** An idiomatic struct, class, interface, or dataclass, with doc comments carried over from the schema.
- **A runtime validator.** One validator per type, applied when a value is parsed off the wire and again when it is serialized onto it. See [Validation guarantees](#validation-guarantees) for more.
- **A [Nexus Service Contract](/glossary#nexus-service-contract) definition**, for a file that declares Services. These are the Service and Operation declarations you register on a Worker and call from a caller Workflow. A pure JSON Schema file declares none, so it produces only the models and their validators.

Additionally, constraint failures are aggregated into a single native error listing every violation, each naming the offending field and the bound it broke.
A handler maps that error to a `BAD_REQUEST` [Nexus error](/nexus/error-handling), so a malformed request tells the caller everything that was wrong with it in one response.

The supported schema subset is deliberately strict.
Anything ambiguous, or anything that cannot be expressed identically in all generated languages, is rejected when you run the generator with a diagnostic explaining how to express it correctly.
The generator prefers to fail loudly at generation time over emitting code that behaves differently in one language than another.

## Supported languages

This page covers the TypeScript output from `nexgen`.

## Definition files

Types are modeled with [JSON Schema 2020-12](https://json-schema.org/draft/2020-12).
A definition file is one of two kinds, decided by what sits at its root.
A file is one or the other, never both.

**Pure JSON Schema.** The root of the document is itself a type, and reusable types live under `$defs`.
Use this when you only need data models shared across languages, with no Service or Operation declarations.

**Nexus document.** Add a root `nexusrpc: "1.0.0"` marker to enable a `services` section.
The root becomes an envelope: Services and their Operations sit at the top level, and your types live under `$defs`.
Only this kind can declare a Service.

The two kinds compose across files, so a contract is not limited to one of them.
A Service contract is often a Nexus document declaring the Services and Operations, plus pure JSON Schema files holding the types it `$ref`s by relative path.
The [`kb/`](https://github.com/temporalio/nexgen/tree/main/samples/schemas/kb) closure described below is built that way.

The examples on this page use [`samples/schemas/chat.nexusrpc.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/chat.nexusrpc.yaml) from the repository, abbreviated here:

```yaml
nexusrpc: '1.0.0'
$schema: https://json-schema.org/draft/2020-12/schema
services:
  ChatService:
    fqn: example.chat.v1.ChatService
    description: Send messages and look up rooms.
    operations:
      sendMessage:
        description: Post a message to a room.
        input: { $ref: '#/$defs/SendMessageInput' }
        output: { $ref: '#/$defs/SendMessageOutput' }
      getRoom:
        description: Look up a room by id.
        input:
          type: object
          additionalProperties: false
          properties:
            roomId: { type: string }
          required: [roomId]
        output: { $ref: '#/$defs/Room' }
      ping:
        description: Liveness probe.
$defs:
  SendMessageInput:
    type: object
    additionalProperties: false
    properties:
      roomId: { type: string }
      message: { $ref: '#/$defs/Message' }
    required: [roomId, message]
  SendMessageOutput:
    type: object
    additionalProperties: false
    properties:
      messageId: { type: string }
    required: [messageId]
```

See [Definition files](https://github.com/temporalio/nexgen#definition-files) in the generator's README for details on that file.

### How names are derived

You write two kinds of name in the schema: one for the Service, and one for each Operation. In the sample above they are `ChatService` and `sendMessage`:

```yaml
services:
  ChatService: # the Service name
    operations:
      sendMessage: # the Operation name
```

Those are the only names you write. From each one the generator produces two more: a wire name and a name in your code. You declare neither of them.

Give each name the casing that matches what it becomes:

- A **Service** name is PascalCase: `ChatService`. A Service becomes a type in the generated code, and types are PascalCase.
- An **Operation** name is camelCase: `sendMessage`. An Operation becomes a method on that type, and the generator cases it like any other member.

The first letter is the part the generator enforces: a Service has to start uppercase and an Operation lowercase. Both must begin with a letter and contain only letters and digits.

```
service name `chatService` must match `^[A-Z][a-zA-Z\d]+$` (start uppercase, then
letters/digits); set the wire name via `fqn` if it must differ
```

> **📝 Note:**
> Overriding the wire name
>
> The `fqn` in that error — a fully qualified name — is optional, and you can skip it to start.
>
> It lets a Service or Operation carry a wire name of your choosing rather than the one the generator derives from the name you wrote. Because it never becomes a code identifier, it accepts characters a name cannot: that is how a Service gets a wire name of `example.chat.v1.ChatService`, or an Operation one of `poll-messages`. Wire names are covered just below.
>
> Use `fqn` when you need to match a contract that is already published, or when you want a versioned, namespaced wire name. Otherwise leave it out.
>

**The wire name** is the string the caller and the handler exchange, and what appears in Event History and the Temporal UI. Neither side types it — both take it from the generated code.

Unless you override it, the wire name is the name from the definition file converted to PascalCase. In the sample that gives the Service a wire name of **`ChatService`** and the `sendMessage` Operation a wire name of **`SendMessage`**.

In the sample above the Service does override it using `fqn`, so the Service wire name becomes **`example.chat.v1.ChatService`**.

**The name in your code** is what you call. The generator recases the name you wrote to match the conventions of the language it is generating for. With no override in play, the two derived names line up like this:

| You write | Wire name | Java | Go | Python | TypeScript |
| --- | --- | --- | --- | --- | --- |
| `ChatService` | `ChatService` | `ChatService` | `ChatService` | `ChatService` | `chatService` |
| `sendMessage` | `SendMessage` | `sendMessage` | `SendMessage` | `send_message` | `sendMessage` |

Operations become camelCase methods in Java and TypeScript, snake_case in Python, and PascalCase in Go.

An Operation's `input` and `output` are each optional.
The `ping` Operation above declares neither, which generates an Operation that takes and returns nothing.
When present, each must be an object type, so that a field can be added later without breaking the wire format.

The repository holds four example definitions under [`samples/schemas/`](https://github.com/temporalio/nexgen/tree/main/samples/schemas):
[`chat.nexusrpc.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/chat.nexusrpc.yaml),
the feature-diverse [`showcase.nexusrpc.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/showcase.nexusrpc.yaml),
the pure-schema [`temporal.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/temporal.yaml),
and a multi-file closure under [`kb/`](https://github.com/temporalio/nexgen/tree/main/samples/schemas/kb) showing how types split across files resolve through `$ref`.
The `kb/` closure starts at [`kb.nexusrpc.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/kb/kb.nexusrpc.yaml) and pulls in types from its [`content/`](https://github.com/temporalio/nexgen/tree/main/samples/schemas/kb/content) and [`tree/`](https://github.com/temporalio/nexgen/tree/main/samples/schemas/kb/tree) subdirectories.

## Install the generator

Build the `nexgen` binary from source with Cargo, the Rust build tool:

```bash
git clone https://github.com/temporalio/nexgen.git
cd nexgen
cargo build --release
```

The binary lands at `target/release/nexgen`.
Confirm it works and check which targets your build supports:

```bash
./target/release/nexgen --version
./target/release/nexgen --help
```

## Generate code

Every language uses the same shape: `nexgen <language> <input>... --output <dir>`.
Inputs are positional and may be files or directories, so you can pass a whole multi-file schema closure. Some languages have extra flags.

> **📝 Note:**
>
> The output directory name becomes the generated package or module name.
> Name it after your domain, such as `chat`, not after the language.
> Pointing `--output` at a directory named `go` produces `package go`, which is not valid Go.
>

### TypeScript

```bash
nexgen ts samples/schemas/chat.nexusrpc.yaml --output ./chat
```

TypeScript accepts `--date-time-types` to choose how date and time fields are represented in memory. There are three choices:

- `string`, the default, keeps every date and time field as the [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) string that appears on the wire. It adds no runtime dependency and round-trips losslessly, but you parse and compare the strings yourself.
- `date` maps `date-time` fields to a JavaScript `Date`. This is lossy: a `Date` is a UTC instant, so the original offset is folded away and precision is capped at milliseconds.
- `temporal` maps to the [TC39 Temporal API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal), a JavaScript standard for dates and times that is unrelated to Temporal the platform. It preserves the offset and sub-second precision, and requires the `Temporal` global.

The chat schema has no date or time fields, so this command uses [`temporal.yaml`](https://github.com/temporalio/nexgen/blob/main/samples/schemas/temporal.yaml), which has one field for each of `date`, `date-time`, `time`, and `duration`:

```bash
nexgen ts samples/schemas/temporal.yaml --output ./events --date-time-types temporal
```

## Dates, times, and durations

TypeScript's `--date-time-types` is the only place you choose how a date or time is represented. Elsewhere the generator decides: Java uses `java.time`, Python `datetime` and `timedelta`, and Go `time.Time` and `time.Duration`. Two cases hand you the wire string to work with instead of a date type — `format: time` in Java, and every date and time format under TypeScript's default `string` mode.

Whichever type you get, every language writes the same bytes. Dates and times use [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339), which is a profile of [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601). ISO 8601 permits many optional spellings of the same instant, and RFC 3339 narrows them to one so two systems cannot read a timestamp differently. RFC 3339 specifies timestamps rather than durations, so durations follow ISO 8601.

## How validation works

Validation is the one behavior the generated types add, so a call can fail with a contract violation that hand-written types would not have caught. Only the wiring of that validation differs between languages.

| SDK        | How validation reaches the wire                             | Extra step |
| ---------- | ----------------------------------------------------------- | ---------- |
| Go         | Generated `MarshalJSON` and `UnmarshalJSON` on each model    | None       |
| Java       | Generated Jackson serializer and deserializer on each model  | None       |
| Python     | Pydantic model validation                                    | [Use the Pydantic data converter](/develop/python/data-handling/data-conversion#use-pydantic-models) |
| TypeScript | Generated mapper classes                                     | Call the mapper yourself |

In Go, Java, and Python the validator sits in the serialization hook the Temporal data converter already calls, so validation happens on its own once the models are in use.
TypeScript requires an explicit call, covered in [Validate payloads in TypeScript](#validate-payloads-in-typescript).

### Validation guarantees

The two directions do not check the same things.

**Parsing a value off the wire** enforces required fields and every value constraint, aggregating all violations into one error. This is the direction that protects a handler from a malformed request.

**Serializing a value onto the wire** enforces value constraints — lengths, bounds, counts, patterns. It does not report a required field you left unset. The field is omitted from the payload and the peer rejects it, so the failure surfaces as a `BAD_REQUEST` from the other side rather than as a local error at the point you built the object.

For code that catches and logs a violation, see the per-language examples in [Use the generated code](#use-the-generated-code).

## Use the generated code

**Whether the code is generated or written by hand, you use it the same way.** It is a Service definition and a set of types: register it on a Worker, and call it from a caller Workflow exactly as described in your SDK's Nexus guide. The one difference is that generated types validate themselves, so a call can fail with a contract violation for you to catch.

Each example below registers a handler, calls the Operation from a caller Workflow, and catches a validation failure.

### TypeScript

The generator emits a `chatService` Service definition plus, for each type, an interface and a companion `<Type>Mapper` class:

```typescript
export const chatService = nexus.service('example.chat.v1.ChatService', {
  sendMessage: nexus.operation<SendMessageInput, SendMessageOutput>({ name: 'SendMessage' }),
  getRoom: nexus.operation<GetRoomInput, Room>({ name: 'GetRoom' }),
  ping: nexus.operation<void, void>({ name: 'Ping' }),
});
```

Register a handler against that definition with `nexus.serviceHandler(chatService, { ... })`, and create a caller with `workflow.createNexusServiceClient({ service: chatService, endpoint: 'chat-endpoint' })`.

#### Validate payloads in TypeScript

> **⚠️ Caution:**
>
> In TypeScript the generated validator only runs when you call the mapper.
> No generated payload converter exists, so nothing calls it for you.
>

Each generated type comes with a mapper exposing two methods.
`fromIntermediate` validates an untrusted plain value and returns the typed model.
`toIntermediate` validates a model and returns its plain wire form.
Call them at both edges of every Operation, on the handler side and the caller side:

```typescript
const handler = nexus.serviceHandler(chatService, {
  async sendMessage(_ctx, input) {
    const request = new SendMessageInputMapper().fromIntermediate(input);
    const output = { messageId: await store(request) };
    return new SendMessageOutputMapper().toIntermediate(output) as SendMessageOutput;
  },
});
```

The caller side is the mirror image. Map the request out before executing the Operation, and map the result back in when it returns:

```typescript
const client = workflow.createNexusServiceClient({
  service: chatService,
  endpoint: 'chat-endpoint',
});

const wire = new SendMessageInputMapper().toIntermediate(input) as SendMessageInput;
const raw = await client.executeOperation(chatService.operations.sendMessage, wire);
const output = new SendMessageOutputMapper().fromIntermediate(raw);
```

The cast is expected in both examples: `toIntermediate` returns `unknown`, because its result is a plain wire value rather than the model type the Operation declares.

Skipping the mapper is the failure to watch for, because nothing reports it.
The value handed to your handler is typed as the model, since `nexus.operation<SendMessageInput, SendMessageOutput>` declares it that way, but at runtime it is only whatever was deserialized.
A handler that ignores the mapper compiles, type-checks, and returns correct results for valid payloads, while enforcing none of the constraints in your schema.

When a payload does violate the contract, `fromIntermediate` throws a `ValidationError` carrying every violation at once:

```
ValidationError: 2 validation error(s): roomId: required; message.body: expected string
```

The error also exposes a `violations` array of `{ path, reason }` objects, so a handler can convert it into a `BAD_REQUEST` Nexus error with the full list intact.

## Regenerate after a contract change

Generated files carry a `DO NOT EDIT` header and are replaced wholesale on the next run. There is no merge step, so anything you add to them is lost.

Two habits make this safe:

- **Commit generated code and regenerate as its own commit.** The diff then shows exactly what the contract change did to each language.
- **Fix names in the schema, not the output.** When a generated identifier is wrong for your language, set a per-language override in the contract so the fix survives regeneration. See [Naming and overrides](https://github.com/temporalio/nexgen#naming--overrides) for the available keys.

If you generate into the wrong directory, delete what landed there and run the generator again with corrected flags. Do not edit the `package` or module declaration to match where the files ended up as the next run will overwrite it.

## Schema defaults

A property can declare a `default`, which makes it optional for a caller to supply:

```yaml
sampleValue:
  type: integer
  default: 0
```

A caller that leaves `sampleValue` unset sends a payload without the field, and the receiver reads `0`. The default is never written into the payload, so an omitted field stays omitted rather than being filled in before it is sent.

> **⚠️ Caution:**
> Changing a default is a breaking change
>
> The default lives in the generated code, not in the payload. Temporal replays a Workflow by re-reading the payloads already recorded in its [Event History](/encyclopedia/event-history) using whatever code the Worker is running now, so changing a default changes what those recorded payloads mean.
>
> If the value affects which commands the Workflow produces, replaying an in-flight Workflow fails with a [non-determinism error](/troubleshooting/execution-failures#non-determinism-error) — the [deterministic constraints](/workflow-definition#deterministic-constraints) that govern any change to Workflow code apply here too. If it does not affect commands, nothing fails and the behavior changes silently, which is harder to catch.
>
> Treat a default as part of the contract. Adding a replacement field is not sufficient on its own: old payloads omit the new field too, so replay reads that field's default and can still diverge. Any change to how existing payloads are interpreted needs a [Workflow versioning](/workflow-definition#workflow-versioning) plan that keeps in-flight Executions on their original behavior.
>

Because the field is optional, Go and Java give you two members side by side, so nothing depends on remembering that a default exists:

- The field itself, which is empty when the caller omitted it — `getSampleValue()` returns `null` in Java, and `SampleValue` is a `nil *int64` in Go.
- An accessor named after it that substitutes the default — `getSampleValueOrDefault()` and `SampleValueOrDefault()`.

Use the first when you need to know whether the caller supplied a value, and the second when you just want a number.

TypeScript has no accessor. `sampleValue` is `undefined` when unset, and the generator exports a `DEFAULT_SAMPLE_VALUE` constant you apply yourself: `sampleValue ?? DEFAULT_SAMPLE_VALUE`.

Python has neither. Pydantic applies defaults when the model is constructed, so `sample_value` always holds a value and an omitted field reads the same as one explicitly set to `0`.

## Supported schema features

The generator implements a curated subset of [JSON Schema 2020-12](https://json-schema.org/draft/2020-12) chosen so that every accepted construct lowers identically into all generated languages.

Fully supported: `properties`, `required`, `default`, `minProperties` and `maxProperties`, `dependentRequired`, string and numeric bounds, `items`, `minItems` and `maxItems`, `minContains` and `maxContains`, `allOf`, the recognized nullable pattern `oneOf: [{type: T}, {type: "null"}]`, and the `title`, `description`, and `deprecated` annotations.

Partially supported: `type` (single-string form only), `additionalProperties`, `propertyNames`, `const` and `enum` (scalars only), `format`, `pattern` (a portable RE2-safe subset), `multipleOf`, `contentEncoding`, `uniqueItems`, `contains`, `oneOf` (branches must be separable by a decidable selector), and `$ref` with `$defs` (local files only).

Deliberately rejected, because they have no coherent typed lowering across all generated languages: `anyOf`, `not`, `if`/`then`/`else`, `dependentSchemas`, `prefixItems`, `unevaluatedProperties`, `unevaluatedItems`, `contentMediaType`, and `contentSchema`.

For the current per-keyword support table, see the [nexgen README](https://github.com/temporalio/nexgen#supported-json-schema-features).

> **💡 Tip:**
> RESOURCES
>
> - [temporalio/nexgen](https://github.com/temporalio/nexgen) for the generator, its README, and the example schemas.
> - [Nexus Services](/nexus/services) for the Service contract concept.
> - Nexus feature guides for registering Services and calling Operations:
>   [Go](/develop/go/nexus/feature-guide) |
>   [Java](/develop/java/nexus/feature-guide) |
>   [Python](/develop/python/nexus/feature-guide) |
>   [TypeScript](/develop/typescript/nexus/feature-guide)
>
