Aviary
Client

Fetcher & Transports

createFetcher, custom transports (axios), and serializers (superjson).

@dudousxd/nestjs-client provides createFetcher — the typed client the generated api.ts calls. It owns URL building, headers, error mapping (ApiHttpError), and the payload transformer. The actual network call is a pluggable transport.

import { createFetcher } from '@dudousxd/nestjs-client';

const fetcher = createFetcher({
  baseUrl: '/api',
  headers: () => ({ authorization: `Bearer ${getToken()}` }),
});

Bring your own HTTP client

By default the transport is native fetch. Pass transport to use anything else.

Use your existing axios instance

import axios from 'axios';
import { createFetcher, axiosTransport } from '@dudousxd/nestjs-client';

const http = axios.create({ baseURL: '/api', withCredentials: true });

const fetcher = createFetcher({ transport: axiosTransport(http) });

Set the base URL on the axios instance (not createFetcher.baseUrl) so it isn't prefixed twice.

A fully custom transport

A Transport takes a normalized request and returns a normalized response:

import type { Transport } from '@dudousxd/nestjs-client';

const transport: Transport = async (req) => {
  const res = await myClient(req.url, { method: req.method, headers: req.headers, body: req.body });
  return {
    ok: res.ok,
    status: res.status,
    statusText: res.statusText,
    contentType: res.headers.get('content-type'),
    text: () => res.text(),
  };
};

createFetcher({ transport });

superjson & transformer pipelines

A transformer is a { stringify, parse } pair. Pass superjson to round-trip rich types (Date, Map, Set, BigInt) — the server must use the same transformer.

import superjson from 'superjson';
import { createFetcher } from '@dudousxd/nestjs-client';

const fetcher = createFetcher({ transformer: superjson });

You can pass an array to compose a pipeline: a base value↔string serializer first, then string↔string wrappers (compression, encryption) applied in order and unwound on parse.

import superjson from 'superjson';
import { createFetcher } from '@dudousxd/nestjs-client';
import { compress } from './my-compress'; // { stringify, parse } over strings

const fetcher = createFetcher({ transformer: [superjson, compress] });

Bring your own — a transformer is just an object that implements { stringify(value): string; parse(text): T }.

superjson runtime

The transformer above replaces serialization on both directions for every consumer — the server must speak the exact same transformer, so adopting it is an atomic cross-app flip. When you only need to revive rich types in responses (so Date, Map, Set, BigInt round-trip) and want each client to opt in independently, use the dedicated @dudousxd/nestjs-client/superjson subpath instead.

It is built on the fetcher's generic deserialize hook (applied to parsed JSON responses) and a x-superjson header so plain-JSON consumers are never affected:

  • Client sends x-superjson: 1 and deserializes the response with superjson.
  • Server SuperjsonInterceptor superjson-serializes the response only when that header is present; every other request gets plain JSON.

This is the runtime complement to the serialization: 'superjson' config: turn off the compile-time Jsonify wrapping there, and revive the values at runtime here.

superjson, rxjs, and @nestjs/common are optional peer dependencies, pulled in only by the /superjson subpath. Install superjson to use this runtime.

Opt the client in

superjsonFetcherOptions() returns the headers + deserialize pair to spread into createFetcher:

src/lib/api.ts
import { createApi } from '../generated/api';
import { createFetcher } from '@dudousxd/nestjs-client';
import { superjsonFetcherOptions } from '@dudousxd/nestjs-client/superjson';

export const api = createApi(
  createFetcher({ baseUrl: '/api', ...superjsonFetcherOptions() }),
);

Already passing your own headers() (e.g. auth)? Use withSuperjson() — it composes your headers with the x-superjson opt-in header so both are sent:

src/lib/api.ts
import { createFetcher } from '@dudousxd/nestjs-client';
import { withSuperjson } from '@dudousxd/nestjs-client/superjson';

const fetcher = createFetcher(
  withSuperjson({ baseUrl: '/api', headers: () => ({ authorization: token() }) }),
);

Add the server interceptor

Register SuperjsonInterceptor so responses are superjson-serialized only for requests carrying the opt-in header. Plain-JSON consumers (and any app that hasn't flipped yet) are untouched, so superjson can be adopted per-consumer without an atomic migration:

src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { SuperjsonInterceptor } from '@dudousxd/nestjs-client/superjson';

@Module({
  providers: [{ provide: APP_INTERCEPTOR, useClass: SuperjsonInterceptor }],
})
export class AppModule {}

Drop the compile-time Jsonify wrapping

With responses revived at runtime, the raw controller return types are now correct. Set serialization: 'superjson' so the codegen stops wrapping response in Jsonify<...>:

nestjs-codegen.config.ts
export default defineConfig({
  // ...
  serialization: 'superjson',
});

Now api.users.show() resolves to { createdAt: Date } again — and the value really is a Date at runtime.

On this page