npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

typedapi-client-helpers

v0.3.5

Published

Reusable typed OpenAPI client helpers.

Readme

typedapi-client-helpers

A small Fetch runtime and OpenAPI 3 TypeScript generator designed to work with TypedApi.Swagger on ASP.NET Core.

Version 0.3.5 highlights

  • Supports [TypedApiFilterForm] endpoints through the x-typedapi-filter-form OpenAPI extension.
  • Generates grouped filter-form arguments containing normal query values and FilterFormValues.
  • Provides buildFilterQuery<TQuery>() for converting filters without adding pagination or sorting.
  • Reconstructs opted-in closed .NET generic schemas as reusable TypeScript generics.
  • Supports exact generic bindings for direct properties, arrays, collections, and dictionary values.
  • Preserves required-but-nullable properties as required T | null instead of optional values.
  • Generates non-cyclic discriminated unions with literal discriminator fields.
  • Consumes readable generic schema IDs such as ApiPaginationResponseOfProjectModel.
  • Generates method-specific unions from typed ProblemDetails error responses.
  • Validates TypedApi OpenAPI contract version 2.

Filter-form endpoints

TypedApi.Swagger can mark a non-paginated query endpoint with [TypedApiFilterForm].

The attribute can be placed on the query parameter:

[HttpGet("map-items")]
public async Task<ActionResult<IReadOnlyList<MapItemResponse>>> GetMapItems(
    [FromQuery, TypedApiFilterForm] ChargePointMapFilter filter,
    CancellationToken token)
{
    return Ok(await service.GetMapItemsAsync(filter, token));
}

It can also be placed on the endpoint:

[TypedApiFilterForm]
[HttpGet("map-items")]
public async Task<ActionResult<IReadOnlyList<MapItemResponse>>> GetMapItems(
    [FromQuery] ChargePointMapFilter filter,
    CancellationToken token)
{
    return Ok(await service.GetMapItemsAsync(filter, token));
}

Placing the attribute on the query parameter is preferred because it explicitly identifies which parameter represents the filter form.

The generated frontend method groups normal query values and dynamic filters inside one filterForm argument:

export async function chargePointGetMapItems(
  filterForm: {
    query: ChargePointGetMapItemsQueryParams;
    filters?: FilterFormValues<ChargePointGetMapItemsQueryParams>[];
  },
  options: ApiMethodOptions<
    MapItemResponse[],
    HttpValidationProblemDetails | ProblemDetails,
    RequestParams
  > = {},
): Promise<
  ApiResult<
    MapItemResponse[],
    HttpValidationProblemDetails | ProblemDetails
  >
>;

Call the generated endpoint with fixed query values and optional filters:

await chargePointGetMapItems({
  query: {
    west,
    south,
    east,
    north,
    zoom,
  },
  filters: activeFilters,
});

The generated implementation merges both sources before sending the request:

const builtQuery = {
  ...(filterForm.query ?? {}),
  ...buildFilterQuery<ChargePointGetMapItemsQueryParams>(
    filterForm.filters ?? [],
  ),
};

Values produced by buildFilterQuery() are applied after normal query values. This means an active filter with the same property name overrides the value from filterForm.query.

Filter-form endpoints do not automatically add pagination or sorting. Paginated endpoints continue to use buildQuery().

For an endpoint where every query property is optional, the generated argument defaults to an empty object:

export async function searchItems(
  filterForm: {
    query?: SearchItemsQueryParams;
    filters?: FilterFormValues<SearchItemsQueryParams>[];
  } = {},
  options: ApiMethodOptions<
    SearchResult[],
    ProblemDetails,
    RequestParams
  > = {},
);

For endpoints containing required query properties, filterForm and filterForm.query remain required.

Generic contracts

With [TypedApiGeneric] on a backend wrapper, a closed schema such as ApiEnvelope<ProjectModel> becomes one reusable declaration:

export interface ApiEnvelope<T> {
  data: T;
  relatedItems: T[];
  itemsByKey: Record<string, T>;
}

Endpoints then use ApiEnvelope<ProjectModel> rather than a repeated ApiEnvelopeOfProjectModel interface.

Inherited generic wrappers are reconstructed whether Swagger uses allOf or emits a flattened schema:

export type ApiPaginationSortResponse<T> = ApiPaginationResponse<T> & {
  sortBy?: string | null;
  sortDirection: SortDirection;
};

Required and nullable properties

OpenAPI presence and nullability remain separate:

export interface NullabilityContract {
  requiredText: string;
  requiredNullableText: string | null;
  optionalNullableText?: string | null;
}

Discriminated unions

Serializer discriminator mappings produce narrowing unions without cyclic inheritance:

export type NotificationModel =
  | EmailNotificationModel
  | SmsNotificationModel;

export type EmailNotificationModel = NotificationModelBase & {
  emailAddress: string;
  kind: "email";
};

Typed errors

When the OpenAPI operation contains typed error response bodies, generated methods expose the union directly:

ApiResult<
  ProjectModel,
  HttpValidationProblemDetails | ProblemDetails
>

Install

npm install [email protected]

The generator expects an OpenAPI 3.x document. Swagger 2.0 documents are rejected with a clear error.

Configure generation

Add settings to the consuming application's package.json:

{
  "scripts": {
    "generate:api": "typedapi-generate",
    "check:api": "typedapi-generate --check"
  },
  "config": {
    "swaggerUrl": "https://localhost:7000/swagger/v1/swagger.json",
    "apiOutput": "src/api",
    "typedApiSwaggerBackupFile": "swagger/swagger.backup.json",
    "typedApiDownloadTimeoutMs": 15000,
    "typedApiCleanOutput": true,
    "typedApiGenerateMissingOperationIds": false,
    "typedApiMethodNameStyle": "operationId",
    "typedApiPrefixMethodNamesWithController": true
  }
}

Run:

npm run generate:api

Generated output contains:

src/api/
├── generated/
│   ├── data-contracts.ts
│   └── http-client.ts
└── methods/
    └── Products.api.ts

The generator intentionally does not create src/api/index.ts.

Import generated methods and contracts directly from their files:

import { getProducts } from "./api/methods/Product.api";
import type { ProductModel } from "./api/generated/data-contracts";

When upgrading from an earlier 0.3.0 build, the next generation removes the old generated src/api/index.ts.

The Swagger backup belongs to the consuming project, not this npm package.

Frontend method names

Choose the source of generated function names with typedApiMethodNameStyle:

{
  "config": {
    "typedApiMethodNameStyle": "action",
    "typedApiPrefixMethodNamesWithController": true
  }
}
  • "operationId" uses the unique OpenAPI operation ID. The controller-prefix setting does not affect this mode.
  • "action" uses the original ASP.NET controller action name.
  • typedApiPrefixMethodNamesWithController: true is the default in action mode and prefixes the normalized controller name.
  • typedApiPrefixMethodNamesWithController: false removes the controller prefix.

For an OrderController action named GetOrderById:

typedApiPrefixMethodNamesWithController: true  -> orderGetOrderById(...)
typedApiPrefixMethodNamesWithController: false -> getOrderById(...)

Operation-specific types follow the selected name:

true  -> OrderGetOrderByIdParams
false -> GetOrderByIdParams

Action-name mode requires the matching TypedApi.Swagger package, which emits x-typedapi-operation.actionName.

When the prefix is enabled, the controller name is read from x-typedapi-operation.controllerName, the first OpenAPI tag, or the route.

If disabling the prefix creates duplicate names across controllers, generation stops with a clear collision error. Enable the prefix, rename an action, or use "operationId".

CI consistency check

typedapi-generate --check

This generates in memory and exits with an error when committed generated files are missing, stale, or different.

Other modes:

typedapi-generate --strict
typedapi-generate --offline
typedapi-generate --verbose
  • --strict fails instead of using a backup after a Swagger download error.
  • --offline intentionally generates from the configured Swagger backup.
  • --verbose prints additional generator diagnostics.

Generated property names

Generated interface and parameter members lowercase only the first character:

Files -> files
URLValue -> uRLValue
ProductID -> productID
snake_case -> snake_case

The remainder of each property name is preserved.

Generated wire metadata converts local property names back to the exact OpenAPI names for:

  • JSON bodies
  • Multipart forms
  • Query parameters
  • Header parameters
  • Cookie parameters
  • Path parameters

Responses and documented error bodies are converted in the opposite direction.

Generated method shape

Generated methods keep parameters and request bodies in separate positional groups.

Non-body parameters come first, the body comes second, and request options remain last:

await updateWarehouse(
  {
    id: requireId(
      context.warehouseId,
      "Warehouse ID",
    ),
  },
  warehouse,
);

The generated signature is:

export async function updateWarehouse(
  pathParams: UpdateWarehouseParams,
  data: WarehouseRequest,
  options: ApiMethodOptions<
    WarehouseModel,
    ApiHttpError,
    RequestParams
  > = {},
): Promise<ApiResult<WarehouseModel, ApiHttpError>>;

Path operations without a body use only the params object:

await deleteSupplier({
  id: requireId(context.supplierId, "Supplier ID"),
});

Body-only operations accept their payload directly:

await uploadProductFiles({
  files: selectedFiles,
});

Regular query-only operations use a query argument:

await searchProducts({
  search: "charger",
  limit: 25,
});

Endpoints marked with x-typedapi-filter-form use a grouped filterForm argument containing query and optional filters properties:

await searchMapItems({
  query: {
    west,
    south,
    east,
    north,
    zoom,
  },
  filters: activeFilters,
});

Paginated endpoints use their existing filter, page, page-size, and sorting arguments:

await getProducts(
  activeFilters,
  1,
  100,
  "name",
  SortDirection.Ascending,
);

Operations involving headers or cookies without path parameters use requestParams.

All generated parameter interfaces are named OperationNameParams. Nested path, query, headers, cookies, or body wrappers are not generated for regular endpoint methods.

Request options are always last:

await updateWarehouse(pathParams, data, {
  params: {
    signal: abortController.signal,
    timeoutMs: 10_000,
    headers: new Headers({
      Authorization: "Bearer ...",
    }),
  },
  onSuccess: result => {
    console.log(result.response);
  },
  onError: result => {
    console.error(result.error);
  },
});

Filter conversion

Use buildFilterQuery() when filters must be converted without adding pagination or sorting:

const query = buildFilterQuery<ProductQueryParams>(
  activeFilters,
);

Use buildQuery() for paginated endpoints:

const query = buildQuery<
  ProductQueryParams,
  ProductTableRow
>(
  activeFilters,
  page,
  pageSize,
  sortBy,
  sortDirection,
);

buildFilterQuery() supports the same FilterFormValues conversion behavior as buildQuery(), including:

  • Primitive values
  • Minimum and maximum values
  • Lists
  • OptionValue values
  • Booleans
  • Numbers
  • Dates
  • Strings

It does not add:

  • pageNumber
  • pageSize
  • sortBy
  • sortDirection

Typed errors

When the OpenAPI document describes error response bodies, generated methods expose their generated type or union.

When no error schema is documented, the method uses ApiHttpError instead of unknown:

const result = await cancelOrder({
  id,
  reason,
});

if (!result.ok) {
  if (result.error.kind === "http") {
    console.error(
      result.error.status,
      result.error.body,
    );
  } else {
    console.error(result.error);
  }
}

Structured client errors include:

  • Network errors
  • Aborted requests
  • Timeout errors
  • Malformed response errors

Malformed JSON is reported as a structured parse error instead of being cast to the expected response type.

Exceptions thrown by consumer onSuccess and onError callbacks propagate normally.

Runtime configuration

import {
  configureApiClient,
  setSecurityData,
} from "typedapi-client-helpers";

configureApiClient({
  baseUrl: "https://api.example.com",
  timeoutMs: 15_000,
  securityWorker: token =>
    token
      ? {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        }
      : undefined,
});

setSecurityData("access-token");

The runtime also exports:

  • createApiClient
  • request
  • abortRequest
  • mergeHeaders
  • toRequestHeaders
  • toCookieHeader
  • handleApiResponse
  • buildFilterQuery
  • buildQuery
  • toFormData

Backend pairing

Use TypedApi.Swagger 0.3.2 or newer on the backend for:

  • [TypedApiFilterForm]
  • Generic reconstruction
  • Corrected nullability
  • Discriminators
  • Readable schema IDs
  • Typed default errors

The backend package emits x-typedapi-filter-form for opted-in filter endpoints.

It also emits x-typedapi.contractVersion = 2 for contract compatibility. The generator validates the contract version before replacing generated files.