Config
typewire.config.ts in your project root:
import { defineConfig } from "@tahanabavi/typewire-cli";
import { ApiClient } from "@tahanabavi/typefetch";
import { contracts } from "./src/contracts";
export default defineConfig({
typefetch: {
contracts,
createClient: ({ baseUrl, token }) => {
const client = new ApiClient({ baseUrl: baseUrl!, token }, contracts);
client.init();
return client;
},
},
});One section per package, so nothing has to be renamed when a package is added
and a project that only uses typefetch writes only that section. lint and
diff sit at the top level and a section may override them.
Prefer createClient over client: it is what lets --base-url and --token
reach the client, so one config serves local, staging and CI.
transports
typefetch: {
contracts,
transports: [graphqlTransport(), grpcTransport()],
}For the commands that never build a client. method and path exist only on
http endpoints, so anything printing a route has to ask the adapter — and list
has no client to ask. Declaring the adapters here is what lets a gRPC route
print unary user.v1.UserService/GetUser instead of ?, while list stays
runnable against an API that is not up. The built-in http adapter is always
present; only add the extras.
contracts is the only always-required key. A client is required by the
commands that actually make requests, and by nothing else — so list (and
later lint, diff, explain) run against a contract file with no API
reachable at all. That is the difference between a check that runs on every
commit and one that never gets wired up.
Several APIs in one repo
A repo with a dashboard API, an admin API and a landing API declares them as
projects — each with its own contracts, client, middleware and base URL:
export default defineConfig({
// Shared defaults; a project may override any of them.
lint: { rules: { "path-params-declared": "error" } },
projects: {
dashboard: {
typefetch: { contracts: dashboardContracts, createClient: createDashboardClient },
},
admin: {
typefetch: { contracts: adminContracts, createClient: createAdminClient },
lint: { rules: { "duplicate-id": "off" } }, // merged over the shared rules
diff: { baseline: "admin.lock.json" }, // its own API-surface snapshot
},
landing: {
typefetch: { contracts: landingContracts }, // no client — list/lint only
},
},
});One file rather than three, because the alternative is three configs, three
--config flags in every script, and three CI steps that drift apart.
typewire list # every project, labelled
typewire test # every project; any failure fails the run
typewire test --project admin # just one
typewire test --project dashboard,admin # someEvery project runs by default. A CI gate that quietly checked one of three API surfaces and reported green would be worse than no gate. Reports go to a per-project folder for the same reason — sharing one output path means the last project silently overwrites the others and the report describes one API while looking complete.
Commands that need exactly one project refuse to guess: with several
declared and no --project, you get an error naming them rather than a run
against whichever happened to be first.
Omit projects entirely for the single-API case — one API should never cost
the ceremony of naming it. Internally it resolves to one implicit project, so
no command branches on which shape you wrote.
Discovery
Walks up from the working directory, so a package inside a monorepo inherits the
root config without a --config flag in every script. .ts, .mts, .cts,
.js, .mjs and .cjs are all supported; TypeScript ones load through jiti,
with your tsconfig.json path aliases applied — a contract file that
imports @/schemas resolves the way it does everywhere else in your project.
extends
export default defineConfig({
extends: "../../typewire.base.ts", // or a package name
typefetch: { contracts },
});Merged key by key, with the extending file winning. Arrays are replaced rather
than concatenated, so a base's formats: ["markdown", "json", "html"] can be
narrowed to ["json"].
A config as a function
export default defineConfig(({ mode, command, ci }) => ({
typefetch: { contracts },
diff: { baseline: ci ? "typewire.lock.json" : ".typewire/local.lock.json" },
}));So --mode can change the baseline or the report format without a second file.