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

@aventara/client

v1.0.0-rc.12

Published

Development-time generator for Aventara typed remote clients.

Readme

@aventara/client

The development-time generator for Aventara typed remote clients. It fetches a deployed framework's ClientContract from <entrypoint>/_contract, verifies it, and writes a standalone TypeScript client into your project. The generated client is what you ship: at runtime it imports nothing from this package or from @aventara/core.

Every argument and result type in the generated client is core's own derivation: @aventara/core's published declarations, copied into the tree and instantiated over the deployment's ClientContract. The client's types therefore match the server's.

Setting up a frontend: avclient init

npx @aventara/client@rc init      # in your frontend project; pnpm: pnpm dlx @aventara/client@rc init

It asks four things and writes the setup. Each answer can also come from a flag, and --yes takes every default:

| Flag | Question | Default | |---|---|---| | --entrypoint <url> | The server's framework entrypoint | http://localhost:3000/api | | --env-var <NAME> / --no-env-var | Read it from this variable, or write it as a literal | AVENTARA_API_URL | | --generate-at <dir> | Where the client goes | ./src/api | | --package-manager <npm\|pnpm> | | the lockfile's, else the launching one, else npm |

It writes framework.client.ts (or reuses the config the project already has), the variable into .env (only with a variable), "avclient:generate": "avclient generate" into the scripts, and @aventara/client as an exact devDependency at its own version. Then it runs the install (--skip-install prints it instead) and generates the client right away (--skip-generate skips that). A server that does not answer leaves the files in place and names avclient generate; --verbose shows the install's output as it runs.

Each step (the install, loading the config, fetching the ClientContract, emitting, checking and writing) is shown on stderr while it runs: live on a terminal, and one plain line per step in a pipe, in CI or with NO_COLOR. Existing content that differs is listed and replaced only when you confirm, or with --yes; where nobody can be asked, nothing is touched. Commit the generated tree, because regenerating it needs a running server.

Generating a client

Install it as a dev dependency (or let avclient init do it), then add framework.client.ts to the directory you run the generator from:

import { defineClientConfig, env } from "@aventara/client";

export default defineClientConfig({
	entrypoint: env("AVENTARA_API_URL"),
	generateAt: "./src/api",
});

and run:

npx avclient generate [--yes]

The config file is loaded the way Prisma 7 loads prisma.config.ts: framework.client.ts (or .mts, .cts, .js, .mjs, .cjs), in ES module or CommonJS syntax, whatever your package.json's "type". A framework.client.mts written by 0.1.0-pilot.1 keeps working, and avclient init reuses it. Two config files in one directory are refused, naming both.

  • entrypoint is the deployment's framework entrypoint: origin plus mount path, one absolute http(s) URL. It may not carry credentials, because the platform's fetch refuses such a URL and the entrypoint ships inside the client.
  • generateAt is a directory you may share with your own files. The generator owns exactly two entries in it, AvClient.ts and generated/, and never reads, moves or removes anything else there.
  • The .env cascade is read from the current directory before the config is evaluated, highest precedence first: the process environment, .env.<mode>.local, .env.<mode>, .env.local, .env; mode is NODE_ENV, or development. The config reads it through env("NAME"); process.env is never written.
  • Content the generator did not produce in AvClient.ts or generated/ is listed, and you are asked before it is overwritten or removed. That includes what a killed run left in the generated/ it moved aside. --yes answers yes. Where nobody can answer (stdin is not a terminal, as in CI), the run is refused and nothing is touched. A cancelled or refused run still prints every warning it raised.

Your tsconfig decides how the client is written

The client is TypeScript source that your own toolchain compiles, like your own files: tsc, Next.js (Turbopack or webpack), Vite, tsx, Node's type stripping. Its modules import each other the way your project imports its files, read from your tsconfig.json (the nearest one above generateAt, extends included) by Prisma 7's rules:

| Your tsconfig | The client's own imports | You import it as | |---|---|---| | allowImportingTsExtensions or rewriteRelativeImportExtensions (Vite's react-ts template, Node's type stripping) | ./generated/client.ts | ./api/AvClient.ts | | moduleResolution: "bundler" (Next.js) or module: "commonjs" | ./generated/client | ./api/AvClient | | anything else, such as module: "nodenext"/"node16" (NestJS 12) | ./generated/client.js | ./api/AvClient.js (ES module) or ./api/AvClient (CommonJS) |

Under nodenext, the nearest package.json's "type" decides whether the client is checked as an ES module or as CommonJS. Vite's tsconfig.json, which only lists references, defers to the referenced project that includes the client (tsconfig.app.json). The validate step type-checks the client in your project's resolution and module format.

A project with no tsconfig.json is refused: the generated client is TypeScript, and JavaScript projects are not supported yet.

What a run guarantees

A failed run leaves the previous output exactly as it was. The whole tree is written to a staging directory inside generateAt, validated there as one program, and only then moved into place. A run killed part-way is repaired by the next run that writes, before it writes anything. A first run that fails removes the directories it created.

The validate step is a real type check when your project has the optional peer typescript (5.7 to 6). It is resolved from your project, so npx @aventara/client@rc init type-checks too, under a consumer's strictest plausible settings and with nothing outside the tree resolvable. Without it, or under TypeScript 7 (which has no classic compiler API to check with), the check falls back to a parse with Node's own TypeScript parser and says so in one warning. It is never skipped, and the client is still written. The peer range has no upper bound, so a frontend on TypeScript 7 installs it; your own tsc checks the tree when it compiles.

Two runs over one ClientContract write the same bytes, and deleting the client and regenerating it gives the same bytes. Nothing volatile (no timestamp, generator version, host or path) is emitted.

A refusal is one line and exit code 1, with no stack; anything the run warned about before it stopped is printed first. A stack means a defect in the generator.

Calling the API

import avClient, { AvClient, type User, type UserWhere } from "./api/AvClient"; // as you import your own files

const where: UserWhere = { email: { equals: "[email protected]" } };
const users = await avClient.User.find.many({ where, select: ["id", "email", { posts: { select: ["$count"] } }] });
const one: User = await avClient.User.find.unique({ where: { id } });
const maybe = await avClient.User.find.first({ where }); // User | null — a miss is null
  • avClient is the default export: the client of the deployment the tree was generated from. new AvClient({ entrypoint, fetch }) makes another. entrypoint is for a deployment serving exactly the same ClientContract (tests, SSR), and fetch is for a custom fetch.
  • The client has one property per Resource, one per family under it, and one per advertised variant. Only what the ClientContract advertises exists, in the type and at runtime. A Resource named like one of the client's own members (all, tx, transaction, then, constructor, Object.prototype's names) is reached as <name>Model, with one warning; its wire name is unchanged.
  • A call resolves to the data: the record, a list, a count. A first-style miss (A1001) is null. Every other outcome throws the AvError subclass of its code (AvNotFoundError for a strict-unique miss, AvContractMismatchError for a stale client, …), or AvTransportError when no framework answer arrived.
  • Values are application values: bigint, Date, Uint8Array, the client's AvDecimal, JSON values as they are. Arguments are encoded and results revived for you; a value that cannot be read throws AvTransportError.
  • Per-call options (AvCallOptions) are never sent in the body: signal (aborts this call only, see below), headers (the framework's own identity headers are refused), requestId, sent as Aventara-Request-Id and winning over that header in headers, and grouping: false (see below).
  • The named types (User, UserWhere, UserUniqueWhere, UserOrderBy, UserCreateData, UserUpdateData, UserSelect, UserInclude) are named exports, each only when the operation it reads is advertised. A name that would clash walks the rename ladder (User beside a Resource UserWhere becomes UserModel), one warning each.

Calls run when made, together

A call runs when you make it, as fetch does: it checks and encodes its arguments and is queued at once, and a call you never await still runs. A transaction too. Awaiting a call again does not send it again.

The calls made in one tick go out as one request, POST /_all (an Operation Set):

const [campaigns, unread, me] = await Promise.all([
	avClient.Campaign.find.many({ where: { ownerId } }),
	avClient.Message.find.count({ where: { toId: ownerId, readAt: null } }),
	avClient.User.find.unique({ where: { id: ownerId } }),
]); // one request
  • Each call runs on the server exactly as it would alone (validation, exposure, policy, limits, pipelines), concurrently with the others and not atomically: a write that ran stays written whatever the others answered. Each call resolves or throws on its own, as soon as its own answer arrives (the client asks for a streamed answer, Accept: application/x-ndjson, where the runtime can read one, and reads a plain JSON answer otherwise, as on React Native).
  • Together means made before the microtask queue settles: one Promise.all, several async functions run together, avClient.all. No timer is involved, and a call made on its own goes to its own route as before.
  • A group larger than the ClientContract's limits.maxAll (or limits.maxRequestBytes) is split into several requests.
  • Sent alone, never grouped: a call with headers or a requestId (they belong to one request), a call made with { grouping: false }, every call of a client made with new AvClient({ grouping: false }), and a transaction.
  • A server without /_all (it answers A2002) gets the calls again one by one, and that client stops grouping.

Aborting a call

A call's signal aborts that call and nothing else. Aborted before it is sent, it is never sent. Aborted while its group is in flight, it rejects at once with the signal's reason (an AbortError), and the others in the request go on; the request itself is aborted once no call in it still waits for an answer. Sent alone, the abort aborts its request, as before. The server still finishes an operation it has already started: there is no way to cancel a database query once it runs.

Nothing is aborted for you: there is no default timeout and no abort on navigation. Pass the signal your data library gives you (React Query and SWR pass one per query), or your own.

avClient.all

const { campaigns, unread } = await avClient.all({
	campaigns: avClient.Campaign.find.many({ where: { ownerId } }),
	unread: avClient.Message.find.count({ where: { toId: ownerId, readAt: null } }),
});

all takes an object of calls and resolves one value per key, typed per key. It is strict: the first failure rejects it with that call's error and aborts its other calls. That is fail-fast, not atomic: a write that already ran stays written. With { settle: true } every key settles on its own, as { status: "fulfilled", value } or { status: "rejected", reason }, and all never rejects for one key:

const panels = await avClient.all({ campaigns, unread }, { settle: true });
if (panels.unread.status === "rejected") { /* show that panel's error */ }

{ signal } aborts every call of that all; a call inside it is aborted by its own signal or by the all's. Calls grouped beside it but not in it are never touched. The keys are yours and are never sent.

avLazy: describe now, run later

import avClient, { avLazy } from "./api/AvClient";

const posts = avLazy(() => avClient.Post.find.many({ where: { published: true } })); // nothing sent yet
const list = await posts; // sent now (once, in a page)

avLazy(() => call) makes the call on its first then, catch or finally, and settles as that call does. In a page it makes the call once and keeps the answer. On a server, where one module serves every request, it keeps nothing: each await makes the call again. avClient.all accepts it as it accepts a call (its signal aborts that call too).

Cached reads are GETs

A read the deployment caches (a cache({ for, maxAge }) stage on it, which its ClientContract marks) is sent as a GET, so the browser, or a CDN when the stage allows one, may keep its answer:

GET <entrypoint>/_resources/Product/find/many?q=<arguments>&v=<ClientContract hash>
  • q is the arguments' canonical JSON (keys sorted, so the same read is the same URL), percent-encoded, or that JSON deflate-raw'd and base64url'd when that is shorter. Where the runtime has no CompressionStream the plain form is sent.
  • A cached read is never grouped into /_all: each keeps its own URL, its own cache entry. Your headers and requestId go with the GET; it sends no header of its own, so a cross-origin read needs no CORS preflight. Its signal aborts it as any call's does.
  • A URL longer than 4096 characters (the whole URL) is POSTed to the same route instead, uncached, as are arguments canonical JSON cannot write.
  • The URL holds the arguments, so it lands in server and proxy logs and in browser history.

Transactions

When the ClientContract advertises interactive transactions, the client has avClient.tx and avClient.transaction (otherwise neither exists):

const created = avClient.tx.User.create.one({ data: { email: "[email protected]" }, select: ["id"] });
const profile = avClient.tx.Profile.create.one({ data: { bio: "…", userId: created.$ref("id") } });
const [user, row] = await avClient.transaction([created, profile]);

A $ref stands for one field of an earlier step's result. It may be written in a step's data at any depth (a field's value, the key a relation's $connect, $disconnect, $set, $connectOrCreate or $upsert names, the where of a nested $update or $delete, and the data $create, $update and $upsert nest), in its cursor, and anywhere in its where, AND/OR/NOT and relation filters included. A projection's nested where and orderBy take none:

const post = avClient.tx.Post.create.one({ data: { title: "Hi", author: { $connect: { id: created.$ref("id") } } } });
const mine = avClient.tx.Post.find.many({ where: { OR: [{ authorId: created.$ref("id") }, { title: { startsWith: created.$ref("email") } }] } });

Its source field must have the same type as where it is written:

  • for a filter's value, equals and not, a nullable source only where the field is nullable, and a null then matches the rows where the field is unset, as a literal null does;
  • for a unique key, a cursor, a comparison or a text operator, a non-null source;
  • for in, notIn, hasEvery and hasSome, a list field;
  • for has, one value.

A mismatch is a type error, and the server refuses it before anything runs (A2007/V1011).

avClient.tx.… builds a handle and sends nothing. avClient.transaction([...]) sends the plan once, when made, and resolves one result per handle, typed, in order; a failing step throws its error with cause.operation naming the step. $ref binds to the handle's position in the list it runs with. Two mistakes are refused before anything is sent, as core refuses them in process: one handle twice (AvValidationError, A2004/V1001), and a $ref to a handle not in the list (AvValidationError, A2007/V1010). A handle is plain data: any client of the same generated tree may run it.

Custom fetch and authentication

fetch is typed as your platform's own fetch when your lib declares one (DOM, @types/node), so the platform fetch is accepted without a cast; with neither, a structural fetch type stands in. The client adds no headers of its own beyond the protocol's, so authentication goes in a custom fetch:

const client = new AvClient({
	fetch: (input, init) => fetch(input, { ...init, headers: { ...init?.headers, Authorization: `Bearer ${token}` } }),
});

The platform fetch is read when a call is made, not when the client is imported. Importing the client never fails where there is no fetch; a call does, saying why. An entrypoint carrying credentials is refused when the config is resolved.

Aventara Access: $access

When the deployment's ClientContract has the access section (the framework is configured with access), the client has $access:

const answers = await avClient.$access.can([
	{ resource: "Order", action: "update", where: { id: 41 } },
	{ resource: "Refund", action: "create" },
]); // [boolean, boolean], one request to /_access/can
await avClient.$access.signIn({ email, password }); // Access's own route
const me = await avClient.$access.me(); // AvAccessMe | null

can is always there; me, signIn, signUp, signOut and resetPassword, and on Access's kiosk profile switchUser, lock, setPin, enrollDevice and revokeDevice, exactly when the section states them. Each verb calls Access's own route (me with GET and cache: "no-store"), and resolves its result projected onto the section's schema: members the schema does not declare are dropped. AvAccessMe is me's result type. A non-2xx answer from Access throws AvAccessError (status, code, message); can's refusals are AvErrors, as every call's. Without the section there is no $access.

Where Access lives decides where a sign-in's credentials go, so the generator emits an absolute Access URL only when it is on the entrypoint's origin, or on an origin framework.client.ts lists:

export default defineClientConfig({
	entrypoint: env("AVENTARA_API_URL"),
	generateAt: "./src/api",
	accessOrigins: ["https://auth.example.com"],
});

A verb on another origin carries cookies only when your fetch sends them: pass one that does (new AvClient({ fetch: (url, init) => fetch(url, { ...init, credentials: "include" }) })), and have Access allow that origin with credentials (CORS).

Up to date

A rerun asks the deployment conditionally (If-None-Match, the ClientContract hash of the output it finds), but only when that output is intact and its carrier re-verifies. On 304 Not Modified it re-emits from that stored ClientContract with this generator and this entrypoint. Identical bytes print avclient: up to date: … nothing was written. and exit 0; otherwise the output is replaced, and the line says the ClientContract is unchanged; the generator or the entrypoint changed.

What is emitted

Every file opens with a banner stating that it is generated and must not be edited, and with the Biome suppressions a generated file needs. They are emitted rather than configured, so the output stays correct whatever formatter the repository uses. The .d.ts files are core's copied declarations under generated/derivation/ and the named types in generated/types.d.ts; the .ts sources carry their own types.

  • AvClient.ts: the entry point you import: avClient (the default export), AvClient, AvClientOptions, AvCallOptions, AvFetch, AvOperation, avLazy, the named Resource types, the enums, AvDecimal, AvError and its subclasses, AvTransportError, and the types AvCause, AvValidationIssue, AvOperationCode and AvValidationCode.
  • generated/client.ts: the typed client: the class AvClient, AvClientOptions, AvCallOptions and the ready instance avClient. Every argument and result type is an instantiation of core's own derivation.
  • generated/contract.ts: the ClientContract the client was generated against, its served canonical bytes verbatim, as the type ClientContractShape (no runtime byte).
  • generated/derivation/: @aventara/core's published declarations of the argument/result derivation and the call grammar, copied byte for byte (bannered, source-map trailer dropped), so the generated types are core's own.
  • generated/enums.ts: each enum as a union type and a same-named as const object.
  • generated/metadata.ts: the ClientContract hash and protocol version the client is bound to, and the entrypoint it was generated from, which is its default deployment (an entrypoint carrying credentials is refused at resolution).
  • generated/runtime/cached-read.ts: a cached read's GET: its q (canonical JSON, plain or deflate-raw), its v, and the POST past 4096 characters. Emitted only when the ClientContract caches a read.
  • generated/runtime/canonical-json.ts: @aventara/core's canonical JSON writer, its compiled source copied as it is (unchecked: // @ts-nocheck). Emitted only when the ClientContract caches a read.
  • generated/runtime/codec.ts: the nine scalars' wire codecs, and the request-body serializer.
  • generated/runtime/decimal.ts: the framework's AvDecimal, with a constructor, toString and toJSON.
  • generated/runtime/descriptor.ts: what the runtime reads of the contract: the result fields it revives and the relations it follows (the decode table), and the advertised operations.
  • generated/runtime/errors.ts: AvError, its subclasses by code class (AvAuthError, AvConflictError, AvContractMismatchError, AvInternalError, AvNotFoundError, AvProtocolError, AvValidationError), AvTransportError, and the code unions, emitted from core's code arrays instead of retyped.
  • generated/runtime/field-check.evaluator.ts: core's own field-check evaluator, copied byte for byte with its imports pointed at generated/derivation/. Emitted only when the ClientContract carries a check (a field's validators, or a check on a Json Shape node).
  • generated/runtime/field-validators.ts: the ClientContract's checks as data, and the walk that runs them on an operation's arguments before anything is sent. A failure throws the server's A2004 with the same issues. A check the client cannot judge exactly as the server would is left to the server. Emitted only when the ClientContract carries a check.
  • generated/runtime/fingerprint.ts: the operation fingerprint (RFC 8785 canonical JSON, SHA-256, base64url), dependency-free and pinned to core's. Emitted only when transactions are interactive.
  • generated/runtime/operation-set.ts: the calls, queued when made, the grouping of the calls made together into Operation Sets (POST /_all), per-call abort, and avClient.all.
  • generated/runtime/transaction.ts: avClient.tx's deferred handles and avClient.transaction's plan assembly, refusals and one POST to /_transactions. Emitted only when transactions are interactive.
  • generated/runtime/transport.ts: one POST per operation, or one POST /_all per Operation Set (its answer read streamed or whole), with the identity headers, and the response read by the protocol's rule. Arguments are encoded by value, results revived by the decode table, and the platform's own fetch type is accepted. Its primitives are module-private: no public file re-exports them, and AvClient's methods reach them through generated/runtime/operation-set.ts.
  • generated/types.d.ts: the named Resource types, as a declaration file (User, UserWhere, UserUniqueWhere, UserOrderBy, UserCreateData, UserUpdateData, UserSelect, UserInclude), each only when its operation is advertised.

The tree is self-contained: every import in it names a file in it, so it compiles with nothing else installed. Names a TypeScript declaration cannot carry as-is (a reserved word, a name the runtime owns, an enum sharing a Resource's name) are renamed, with one warning each; the wire name is kept.

What a generated client is bound to

A generated client is bound to the exact ClientContract hash and protocol version it was generated from. Both are in generated/metadata.ts, and both are sent on every request (Aventara-Protocol-Version, Aventara-Contract-Hash). A deployment whose ClientContract differs refuses the request with A2005, which the client throws as AvContractMismatchError.

The hash covers everything the deployment advertises, not only its schema. Two deployments of one schema can advertise different ClientContracts, and a client generated against one is refused by the other. Whatever the cause, the remedy is the same: generate against the deployment the client will call, and regenerate when it changes.

The same holds for an operation the deployment no longer advertises. The server checks the client's identity before it routes, so a stale client calling a removed operation also hears A2005 and throws AvContractMismatchError. A response that is not a framework envelope at all, such as a host's own 404 for a path it does not mount, is an AvTransportError. It names no remedy, because nothing in it says the client is stale.

The embedded entrypoint is a default. new AvClient({ entrypoint }) may point a client at another deployment without re-binding anything: the ClientContract hash still decides whether that deployment accepts the client.

Type-checking cost in a consumer

Under skipLibCheck: true (the common default), a consumer pays for the calls it type-checks, not for the schema: the named types (generated/types.d.ts) and core's derivation are declaration files checked only where read. Under skipLibCheck: false every named type is resolved where it is declared, so the cost grows with the number of Resources.

Before 1.0

The Aventara wire prefix of the protocol's headers (Aventara-Protocol-Version, Aventara-Contract-Hash, Aventara-Request-Id) may still change before the first stable release. It is one constant in the generated generated/runtime/transport.ts, and regenerating picks up a change.

Known issues

The generator can read the wrong tsconfig.json. In a monorepo or a non-standard layout, the nearest tsconfig.json above generateAt may not be the one your project compiles the client with. The client's imports or module format then do not match your compiler (Cannot find module './generated/client.js', or an extension your bundler will not resolve). Name the right one in framework.client.ts, relative to the config file:

export default defineClientConfig({
	entrypoint: env("AVENTARA_API_URL"),
	generateAt: "./src/api",
	tsconfigFile: "./tsconfig.app.json",
});

License

PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you may not use it to build a product that competes with Aventara itself. See LICENSE and LICENSE-ADDITIONAL-PERMISSION.md.