A domain-specific language for defining API contracts. Compiles .ck (contractkit) files into TypeScript code with Zod schemas and Koa routers.
pnpm install
pnpm testcontractkit [options]
Options:
-c, --config <path> Path to config file (default: searches for contractkit.config.json)
-w, --watch Watch for changes and recompile
--force Skip incremental cache, recompile allThe compiler searches upward from the current directory for contractkit.config.json.
Create contractkit.config.json in your project root. The CLI itself only handles file discovery, caching, and prettier formatting — all code generation happens in plugins declared under "plugins".
{
"rootDir": ".",
"cache": true,
"prettier": true,
"patterns": ["contracts/types/**/*.ck", "contracts/operations/**/*.ck"],
"plugins": {
"@contractkit/plugin-typescript": {
"server": {
"baseDir": "apps/api/",
"zod": true,
"output": {
"routes": "src/routes/{filename}.router.ts",
"types": "src/modules/{area}/types/{filename}.ts"
},
"servicePathTemplate": "#modules/{module}/{module}.service.js"
},
"sdk": {
"baseDir": "packages/sdk/",
"name": "myapp",
"output": {
"sdk": "src/{name}.sdk.ts",
"types": "src/{area}/types/{filename}.ts",
"clients": "src/{area}/{filename}.client.ts"
}
}
},
"@contractkit/plugin-openapi": {
"baseDir": "docs/api/",
"output": "openapi.yaml",
"info": { "title": "My API", "version": "1.0.0" },
"servers": [{ "url": "https://api.example.com" }],
"security": [{ "bearerAuth": [] }],
"securitySchemes": {
"bearerAuth": { "type": "http", "scheme": "bearer", "bearerFormat": "JWT" }
}
},
"@contractkit/plugin-markdown": {
"baseDir": "docs/",
"output": "api-reference.md"
}
}
}| Field | Type | Description |
|---|---|---|
rootDir |
string |
Base directory for resolving relative paths. Supports ~ for $HOME. Default: . |
cache |
boolean | string |
Enable on-disk caching (build hashes + fetched HTTP responses). Pass a string to override the cache directory (default: .contractkit/cache). Default: false |
prettier |
boolean |
Format generated TypeScript files with your local prettier. Default: false |
patterns |
string[] |
Glob patterns for .ck files to compile, relative to rootDir |
plugins |
object |
Map of plugin package name → options. See plugins below. |
Each plugin is its own npm package and is loaded by listing it under "plugins". The value of each entry is passed to the plugin as ctx.options.
| Package | Generates |
|---|---|
@contractkit/plugin-typescript |
Koa routers, TypeScript SDK clients, Zod schemas, plain TS types |
@contractkit/plugin-openapi |
OpenAPI 3.0 YAML |
@contractkit/plugin-markdown |
Markdown API reference |
@contractkit/plugin-bruno |
Bruno REST collection |
@contractkit/plugin-python |
Python SDK client (Pydantic v2 + httpx) |
Has up to four optional sub-configs. Each is independent — include only the ones you need.
Generates Koa router files from operation declarations. Optionally also emits Zod schemas or plain TypeScript types from contract declarations (used for typing route handlers).
| Field | Type | Description |
|---|---|---|
baseDir |
string |
Directory (relative to rootDir) where server files are written |
zod |
boolean |
When true, output.types emits Zod schemas. When false/omitted, emits plain TypeScript interfaces. |
output.routes |
string |
Path template for Koa router files. Default: {filename}.router.ts |
output.types |
string |
Path template for type/schema files |
servicePathTemplate |
string |
Import path template for service implementations. Supports {module}. |
includeInternal |
boolean |
Whether to emit handlers for internal operations. Default: true. |
Generates a typed TypeScript HTTP client. Each operation file becomes a client class; an aggregator class plus a shared sdk-options.ts runtime helper file are emitted automatically. Operations marked internal are excluded from the SDK by default — set includeInternal: true for an internal-use SDK.
| Field | Type | Description |
|---|---|---|
baseDir |
string |
Directory (relative to rootDir) where SDK files are written |
name |
string |
Used for the aggregator SDK class name (e.g. "myapp" → MyappSdk) |
zod |
boolean |
When true, output.types emits Zod schemas. When false/omitted, emits plain TypeScript interfaces. |
output.sdk |
string |
Path template for the SDK aggregator file. Supports {name}. Default: sdk.ts |
output.types |
string |
Path template for SDK type files |
output.clients |
string |
Path template for client class files |
includeInternal |
boolean |
Whether to emit SDK methods for internal operations. Default: false. |
Standalone generators that emit one Zod (or plain TS) file per .ck source file. Use these when you don't need a router or SDK — just schemas/types.
| Field | Type | Description |
|---|---|---|
baseDir |
string |
Directory (relative to rootDir) where files are written |
output |
string |
Path template. Default: {filename}.schema.ts (zod) or {filename}.types.ts (types) alongside source |
All path templates support {filename}, {dir}, {area}, and (for output.sdk) {name}. {area} resolves to the area value declared in the source file's options { keys: { area: ... } } block.
| Field | Type | Description |
|---|---|---|
baseDir |
string |
Directory for the output file |
output |
string |
Output filename. Default: openapi.yaml |
info |
object |
OpenAPI info block (title, version, description) |
servers |
array |
List of { url, description } server entries |
security |
array |
Global OpenAPI security requirement |
securitySchemes |
object |
Map of scheme name → OpenAPI security scheme (e.g. { type, scheme }) |
includeInternal |
boolean |
Whether to document internal operations. Default: false. |
Only types referenced by emitted operations are included.
| Field | Type | Description |
|---|---|---|
baseDir |
string |
Directory for the output file |
output |
string |
Output filename. Default: api-reference.md |
includeInternal |
boolean |
Whether to render internal operations. Default: false. |
Unreachable types are excluded.
| Field | Type | Description |
|---|---|---|
baseDir |
string |
Directory for the output collection |
output |
string |
Output directory name. Default: bruno-collection |
collectionName |
string |
Bruno collection name. Default: the rootDir basename |
auth |
object |
{ defaultScheme, schemes } — schemes use the same shape as OpenAPI security schemes plus hmac |
includeInternal |
boolean |
Whether to generate request files for internal operations. Default: true. |
environments |
object |
Map of environment name → variables. Each entry produces a environments/<name>.yml file. |
Regenerates the output directory cleanly on each run.
| Field | Type | Description |
|---|---|---|
baseDir |
string |
Output directory relative to rootDir. Default: python-sdk |
packageName |
string |
Used in the aggregator class name. Default: Sdk |
includeInternal |
boolean |
Whether to emit client methods for internal operations. Default: false. |
Emits one Pydantic v2 module per contract file and one httpx client per operation file. Method names follow the same priority as the TS SDK (sdk: → name: → derived from HTTP verb + path), converted to snake_case.
Plugins implement the ContractKitPlugin interface from @contractkit/core. Hooks: transform (mutate AST per file), validate (throw to fail compilation), generateTargets (emit output files), and command (register a CLI subcommand). See packages/contractkit/src/plugin.ts.
Contract files use the .ck extension. A file can contain an optional options block followed by any number of contract and operation declarations in any order.
options { ... } # optional — file metadata
contract Foo: { ... } # type declarations
contract Bar: Foo & { ... }
operation /path: { ... } # route declarations
Declares file-level metadata: key/value pairs, service import paths, security defaults, and global request/response headers.
options {
keys: {
area: ledger
}
services: {
LedgerService: "#src/modules/ledger/ledger.service.js"
}
request: {
headers: {
authorization: string
x-request-id?: uuid
}
}
response: {
headers: {
x-request-id: uuid
}
}
security: {
roles: admin
}
}
keys— arbitrary key/value pairs attached to the file's metadata (e.g.areais used for grouping in generated docs). Any key can also be referenced from any string in the file as{{name}}; see Variable substitution.services— maps service identifiers to import paths; used inservice:bindings within operations. Paths starting with#are resolved as package-relative imports.request: { headers }— request headers applied to every operation in the file. Op-level headers with the same name override; an operation can opt out entirely withheaders: none. A name collision with a path parameter raises an error.response: { headers }— response headers applied to every status code on every operation. Per-status override isheaders: { same-name: <type> }; per-status opt-out isheaders: none. Note: OpenAPI and Markdown reflect these on every status; the TS server (ctx.set) and SDK return shape only emit headers on the primary response (first body-bearing response), matching existing inline-headers behavior.security— file-level default security applied to all operations unless overridden at the route or operation level. Accepts the same syntax as operation-levelsecurity:blocks.
Any string in a .ck file can reference a value from options.keys with {{name}}:
options {
keys: { bruno: "../../bruno" }
}
operation /auth/token: {
post: {
plugins: { bruno: "{{bruno}}/authentication/request.token.yml" }
response: { 201: { application/json: AuthenticationToken } }
}
}
Behavior:
{{name}}resolves tooptions.keys[name]first, then to a workspace-wide fallback collected by the CLI from each plugin'skeysconfig incontractkit.config.json. If neither layer defines the name, the literal stringundefinedis emitted and a warning is raised.\{{name}}escapes the substitution; the literal characters{{name}}are emitted with no warning.- Substitution applies to every string field in the AST except
options.keysitself — keys are not recursively expanded.
contract declares a named type that compiles to a Zod schema and a TypeScript type.
contract User: {
id: readonly uuid
name: string
email: email
age?: int
role: enum(admin, member) = member
active: boolean = true
}
Use & to extend one or more base models. The generated Zod schema uses chained .extend()s, OpenAPI emits allOf, plain TypeScript emits extends (with Omit<...> per base when fields are overridden), and Python emits a comma-separated parent list.
contract Admin: User & {
permissions: array(string)
department: string
}
Multi-base — list bases left-to-right, inline block last:
contract Test5: Test1 & Test2 & Test3 & Test4 & {
e: string
}
When two or more bases declare a field with the same name and same shape, no action is needed — the duplicate is silently deduplicated.
When two or more bases declare a field with the same name but different shape (different type, optionality, nullability, visibility, default, or deprecation), this is a conflict. The model must redeclare that field in its inline block with the override modifier; otherwise compilation fails:
contract A: { x: string }
contract B: { x: int }
contract C: A & B & {
x: override int # required — bases disagree
}
override also acts as a deliberate redeclaration when extending a single base — it makes shadowing intent explicit. It must shadow at least one base-contributed field; using override on a name that no base declares is an error.
The override declaration fully replaces the field — visibility, optionality, defaults, and deprecation flags from the base are not inherited. Re-add them on the override line if needed:
override x: readonly int = 0
A contract that maps directly to a type expression — no braces, no fields.
contract UserId: uuid
contract Status: enum(active, inactive, pending)
contract Tags: array(string)
contract MaybeUser: User | null
A trailing # comment on a type alias becomes the schema's .describe() string:
contract OfferStatus: enum(active, accepted, declined, expired) # The status of the offer
Modifiers appear between the contract keyword and the model name, in any order.
Marks the entire type as deprecated.
contract deprecated LegacyUser: {
id: uuid
username: string
}
Effect:
- Emits
/** @deprecated */JSDoc on the generated schema and TypeScript type - Sets
deprecated: truein the OpenAPI schema object - Adds a deprecation notice in generated markdown docs
Controls how Zod handles unknown keys on the object schema. Default is strict.
contract mode(strip) UserInput: {
name: string
email: email
}
| Mode | Zod Method | Behavior |
|---|---|---|
strict |
z.strictObject |
Rejects unknown keys (default) |
strip |
z.object |
Silently removes unknown keys |
loose |
z.looseObject |
Passes unknown keys through |
Applies a key-casing transform when parsing input and/or serializing output. Useful for external data sources that use a different naming convention than the application's internal camelCase convention.
contract format(input=camel) mode(loose) WebhookPayload: {
eventType: string
createdAt: datetime
organizationId: uuid
}
With format(input=camel), the schema accepts camelCase keys (e.g. eventType) and transforms them to the internal camelCase representation. Use format(input=snake) to accept snake_case keys, or format(input=pascal) to accept PascalCase keys.
format(output=snake) transforms the output keys from internal camelCase to snake_case before serialization. Both args can be combined:
contract format(input=pascal, output=snake) ExternalEvent: {
eventType: string
createdAt: datetime
}
This accepts PascalCase input keys and emits snake_case output keys.
Multiple modifiers may appear in any order:
contract deprecated format(input=camel) mode(strip) OldWebhookPayload: {
eventType: string
}
| Type | Zod Output | Notes |
|---|---|---|
string |
z.string() |
|
number |
z.coerce.number() |
|
int |
z.coerce.number().int() |
|
bigint |
z.coerce.bigint() |
|
boolean |
z.coerce.boolean() |
|
date |
z.string().date() |
ISO 8601 date string |
time |
z.string().time() |
ISO 8601 time string |
datetime |
Luxon DateTime |
Full ISO 8601 datetime |
interval |
Luxon Interval |
ISO 8601 interval (e.g. 2024-01-01/2024-12-31); serialized back to ISO string |
email |
z.string().email() |
|
url |
z.string().url() |
|
uuid |
z.string().uuid() |
|
unknown |
z.unknown() |
|
null |
z.null() |
Typically used in union: T | null |
object |
z.object({}) |
Untyped/passthrough object |
binary |
z.custom<Buffer>(...) |
Node.js Buffer validation |
json |
Recursive _ZodJson |
Any JSON-serializable value |
Compound types take arguments in parentheses. Arguments may be type expressions, key=value constraint pairs, or literals.
| Syntax | Zod Output |
|---|---|
array(T) |
z.array(T) |
array(T, min=1, max=10) |
z.array(T).min(1).max(10) |
tuple(A, B, C) |
z.tuple([A, B, C]) |
record(K, V) |
z.record(K, V) |
enum(a, b, c) |
z.enum(["a", "b", "c"]) |
literal("val") |
z.literal("val") |
literal(42) |
z.literal(42) |
literal(true) |
z.literal(true) |
lazy(T) |
z.lazy(() => T) |
discriminated(by=k, A | B | C) |
z.discriminatedUnion("k", [A, B, C]) |
Scalar types accept constraint arguments in parentheses:
contract Validated: {
slug: string(min=1, max=50, regex=/^[a-z0-9-]+$/)
code: string(length=3)
score: number(min=0, max=100)
count: int(min=1)
tags: array(string, min=1, max=20)
}
| Constraint | Applies To | Description |
|---|---|---|
min=N |
string, number, array | Minimum length / value / count |
max=N |
string, number, array | Maximum length / value / count |
length=N |
string | Exact string length |
regex=/pattern/ |
string | Regex pattern validation. Patterns without ^/$ are auto-anchored for full-match semantics; patterns with explicit anchors are emitted as-written. |
format=name |
string | Named format hint (passthrough) |
Types can be composed with | (union) and & (intersection):
contract Response: {
data: User | Team | null
meta: Pagination & { total: int }
}
A | Bcompiles toz.union([A, B])A & Bcompiles toA.and(B)— or.extend()when one side is an inline object and the other is a model reference
A leading | is permitted so multi-line unions read cleanly:
contract AuthenticationRequest:
| ClientCredentialsAuthenticationRequest
| PasswordAuthenticationRequest
| RefreshTokenAuthenticationRequest
| LinkAuthenticationRequest
| OtpAuthenticationRequest
| FidoAuthenticationRequest
When every member of a union carries a shared literal field, wrap it in
discriminated(by=<field>, ...) to emit a faster, narrower runtime check
and a richer OpenAPI schema:
contract CardPayment: { kind: literal("card"), last4: string(len=4) }
contract BankPayment: { kind: literal("bank"), accountId: string }
contract WirePayment: { kind: literal("wire"), swift: string }
contract PaymentMethod:
discriminated(by=kind, CardPayment | BankPayment | WirePayment)
What you get:
| Output | Result |
|---|---|
| Zod | z.discriminatedUnion("kind", [CardPayment, BankPayment, WirePayment]) |
| TypeScript | CardPayment | BankPayment | WirePayment (TS narrows on kind) |
| OpenAPI | oneOf with a discriminator: { propertyName, mapping } block |
| Python (SDK) | Annotated[Union[...], Field(discriminator="kind")] (Pydantic v2) |
The compiler validates discriminated unions at parse time:
- Every member must be a model reference or inline object
- Every member must contain a field matching the discriminator name
- That field must be a
literal(...)orenum(...)type - At least two members are required
Failing any check produces a warning that points to the offending member.
Fields follow the pattern:
name?: [modifiers] TypeExpression [= defaultValue] # optional comment
? after the field name marks it optional:
nickname?: string
Compiles to .optional() on the field's schema.
Include null in a union to allow null values:
middleName: string | null
deletedAt: datetime | null
Compiles to .nullable() on the field's schema.
Modifiers appear after : and before the type expression, in any order.
readonly — present only in the read schema (excluded from write/input). Use for server-generated values:
id: readonly uuid
createdAt: readonly datetime
writeonly — present only in the write/input schema (excluded from read). Use for secrets:
password: writeonly string
deprecated — marks the field as deprecated. Can be combined with readonly/writeonly in either order:
legacyId: deprecated string
token: deprecated writeonly string
apiKey: writeonly deprecated string # order doesn't matter
Effect: emits /** @deprecated */ in generated TypeScript, sets deprecated: true in OpenAPI property schema.
When a model contains readonly or writeonly fields, the compiler generates three schemas:
ModelBase— all fields (internal, used for.extend())Model— read schema (omitswriteonlyfields)ModelInput— write schema (omitsreadonlyfields)
status: enum(active, inactive) = active
retries: int = 3
label: string = "untitled"
enabled: boolean = true
Compiles to .default(value) on the schema.
Fields can declare anonymous nested objects inline. Mode modifiers are supported:
contract Order: {
id: uuid
address: {
street: string
city: string
zip: string(length=5)
}
metadata: mode(strip) {
source: string
campaign?: string
}
}
Inline objects also support intersection with a model reference:
query: Pagination & {
status?: array(Status)
from?: date
}
# starts a line comment. Comments are contextually attached to the node they precede or follow inline.
# Represents an authenticated user
contract User: {
id: readonly uuid # server-assigned identifier
name: string # full display name
email: email
}
- A
#comment on the line before acontractbecomes the model's.describe()string and appears in generated docs - A
#comment on a type alias line becomes its description:contract Status: enum(a, b) # desc - A
#comment inline on a field (same line) becomes the field's.describe()string - A
#comment on the line before a field becomes that field's description
operation declares a route with one or more HTTP method handlers. Compiles to a Koa router.
operation /path: {
get: { ... }
post: { ... }
put: { ... }
patch: { ... }
delete: { ... }
}
Modifiers use function-call syntax on the operation keyword:
operation(internal) /admin/users: { ... }
operation(deprecated) /v1/users: { ... }
| Modifier | Effect |
|---|---|
internal |
By default: excluded from SDK / Python SDK / OpenAPI / Markdown output, included in the server router and Bruno collection. Each plugin accepts an includeInternal: boolean config option to override its default. |
deprecated |
Adds @deprecated JSDoc and deprecated: true in OpenAPI output for all operations on this route. |
Route-level modifiers cascade to all operations. Individual operations can override using the same modifier syntax on the HTTP method verb (see below).
Declare path parameters with {paramName} in the route path:
operation /users/{id}: {
params: {
id: uuid
}
get: { ... }
}
Multiple parameters:
operation /orgs/{orgId}/members/{memberId}: {
params: {
orgId: uuid
memberId: uuid # the member to fetch
}
get: { ... }
}
The params block can also reference a named contract type:
operation /users/{id}: {
params: UserParams
get: { ... }
}
An objectMode modifier can be applied to the params block:
params: mode(strip) {
id: uuid
}
Path parameter types accept the full type-expression syntax — including constraints, enums, and unions:
operation /orders/{orderId}: {
params: {
orderId: int(min=1, max=5)
}
get: { ... }
}
operation /pets/{status}: {
params: {
status: enum(available, pending, sold)
}
get: { ... }
}
The compiler validates that every {param} in the path has a corresponding entry in the params block and warns on mismatches. Path parameters are compiled to Koa :param syntax in the generated router.
Each HTTP verb opens a block with its operation details. An inline # comment after { becomes the operation's description:
get: { # list all active users
service: UserService.list
...
}
A # comment on the line before a verb also becomes its description:
# Create a new user
post: {
service: UserService.create
...
}
Apply a modifier to a specific verb:
operation(internal) /admin/users: {
get(public): { # overrides route-level internal — this one IS in the SDK
response: { 200: { application/json: array(User) } }
}
post: {} # still internal
delete(deprecated): {} # internal AND deprecated
}
| Modifier | Scope | Effect |
|---|---|---|
internal |
operation | Overrides a route-level public or no modifier to make this operation internal. |
deprecated |
operation | Marks this operation deprecated in OpenAPI and JSDoc. |
public |
operation only | Overrides a route-level internal modifier to make this specific operation public. |
Declare query parameters inline or by reference:
get: {
query: {
page?: int
limit?: int = 20
search?: string
}
}
Reference a named type:
get: {
query: PaginationQuery
}
Intersection with inline additions:
get: {
query: Pagination & {
status?: array(Status)
from?: date
to?: date
}
}
Apply an object mode to control unknown key handling:
get: {
query: mode(strip) {
page?: int
}
}
post: {
headers: {
authorization: string
x-request-id?: uuid
x-idempotency-key?: string
}
}
Or by type reference, with optional mode:
post: {
headers: mode(strip) WebhookHeaders
}
post: {
request: {
application/json: CreateUserInput
}
}
Supported content types: application/json, multipart/form-data.
Inline body types are supported:
post: {
request: {
application/json: {
name: string
email: email
}
}
}
get: {
response: {
200: {
application/json: User
}
}
}
Multiple status codes:
post: {
response: {
201: {
application/json: User
}
422: {
application/json: ValidationError
}
}
}
No-body response (status only):
delete: {
response: {
204:
}
}
Each status code can declare typed response headers alongside the body. Names use the on-the-wire form (hyphens allowed, case-insensitive). The ? suffix marks a header optional.
get: {
response: {
200: {
application/json: Transfer
headers: {
preference-applied?: string
vary?: string
etag: string # cache validator
}
}
}
}
Generated effects:
- OpenAPI emits
headers:under each response with the schema and required flag. - TypeScript SDK changes the method's return shape from
Promise<T>toPromise<{ data: T; headers: { preferenceApplied?: string; ... } }>(orPromise<{ headers: ... }>for void responses). Header names are camelCased; values are read from theHeadersobject as strings (nullbecomesundefined). - TypeScript router types the service method's return as
{ body, headers }(or{ headers }for void), and the wrapper callsctx.set(name, String(value))for each declared header. - Python SDK generates a per-method
TypedDict(e.g.GetTransferHeaders) and changes the return type totuple[T, GetTransferHeaders](orGetTransferHeadersfor void). Header keys are snake_cased; values come from the lower-cased response-header dict. - Bruno adds an
isDefinedruntime assertion for each required response header on the asserted status code, and lists all declared headers in the request'sdocsblock. - Markdown docs render a
Response headerstable per status code.
Operations without a headers block on their response keep the current return shape — this change is opt-in per response.
Security can be declared at the file level (inside the options block), at the route level, or at the operation level. It cascades from operation → route → file → config default.
Explicitly public (no auth required):
post: {
security: none
...
}
Require specific roles (for RBAC schemes):
get: {
security: {
roles: admin editor
}
...
}
Named scheme (references a scheme defined in config):
post: {
security: {
webhookAuth
}
...
}
Route-level security applies to all operations in the route unless overridden:
operation /admin/users: {
security: {
roles: admin
}
get: { ... } # requires admin role
post: { ... } # requires admin role
delete: {
security: {
roles: superadmin # overridden — requires superadmin
}
...
}
}
Binds the operation to a service method. The service name must be declared in the options block.
post: {
service: UserService.create
...
}
The generated router imports and calls UserService.create(ctx).
By default the SDK method name is derived from the route path and HTTP verb. To override it explicitly:
get: {
sdk: getById
service: UserService.getById
...
}
For HMAC-authenticated webhooks, bind the operation to a signature key:
post: {
signature: MODERN_TREASURY_WEBHOOK
security: none
headers: WebhookHeaders
request: {
application/json: unknown
}
response: {
204:
}
}
The signature value must match an HMAC scheme name in the config. The generated router middleware validates the HMAC signature before the handler runs.
An operation can attach plugin-specific configuration via the plugins: block. Each entry maps a plugin name to a JSON-like value (string, number, boolean, null, object, array) — the plugin owns its schema for that value:
post: {
plugins: {
bruno: {
template: "file://request-token.yml"
}
}
request: {
application/json: AuthRequest
}
response: {
200: { application/json: AuthResponse }
}
}
Any string starting with file:// is treated as a path relative to the .ck file, and any string starting with http:// or https:// is fetched via GET; in both cases the CLI replaces the URL with the response body before plugins run. The original (raw) tree lives at op.plugins; the resolved tree lives at op.pluginExtensions. Missing files, network errors, and non-2xx responses emit a warning and leave the URL string in place.
When the build cache is enabled, successful HTTP responses are persisted under <rootDir>/.contractkit/cache/http/ (keyed by URL hash) and reused on subsequent runs without hitting the network. The build hash cache lives next to it at <rootDir>/.contractkit/cache/build.json. Add .contractkit/ to .gitignore. Pass --force (or set cache: false) to bypass both caches. Each unique URL is also deduplicated within a single run.
Plugins can validate their entry shape at compile time by implementing validateExtension(value) on the ContractKitPlugin interface and returning { errors?: string[]; warnings?: string[] }. The CLI matches each entry's key against each plugin's name and runs the validator post-resolution. The Bruno plugin uses this to enforce a { template?: string } shape and reject unknown fields.
This is the escape hatch for cases where a plugin's generated output needs to be replaced or augmented with hand-authored content (for example, a Bruno request that needs a post-response script to extract an auth token).
The TypeScript SDK is produced by the sdk sub-config of @contractkit/plugin-typescript. The aggregator class, barrel exports, and a shared sdk-options.ts runtime helper are emitted automatically.
import { MyappSdk } from '@myapp/sdk';
const sdk = new MyappSdk({ baseUrl: 'https://api.example.com' });
const users = await sdk.users.list({ query: { page: 1 } });keys.area and keys.subarea (set in a file's options { keys: { ... } } block) drive how operations cluster on the generated SDK:
| File metadata | Generated layout |
|---|---|
area: identity, subarea: invitations |
IdentityInvitationsClient emitted as a leaf file; exposed as sdk.identity.invitations.<method> |
area: identity (no subarea) |
methods inlined directly on IdentityClient (no standalone *.client.ts); exposed as sdk.identity.<method> |
| neither | flat top-level property — sdk.<filename>.<method> (legacy behavior) |
Multiple files mapping to the same (area, subarea) are merged into one client. Multiple area-level files contributing methods that collide on name fail at codegen time with a clear error — disambiguate with sdk: or move one into a subarea.
{subarea} is available as a path-template variable on output.clients and output.types alongside {area}, {filename}, and {dir}. Example: output.clients: "src/{area}/{subarea}.client.ts" produces src/identity/invitations.client.ts.
A Python SDK with the same operation coverage is available via @contractkit/plugin-python.
OpenAPI 3.0 YAML and Markdown reference are produced by the @contractkit/plugin-openapi and @contractkit/plugin-markdown plugins respectively. In both, operations marked internal and any types unreachable from public operations are excluded.
A Bruno REST collection can be generated via @contractkit/plugin-bruno.
The compiler caches file hashes and skips unchanged files on subsequent runs. Set "cache": true in your config to enable. The cache directory (.contractkit/cache by default) holds both build hashes (build.json) and any fetched plugin extension HTTP responses (http/); pass a string for cache to override the directory. Use --force to bypass the cache and recompile everything.
The compiler validates type references across files. If a field or operation references a model that doesn't exist in any parsed file, a warning is emitted.
Set "prettier": true in your config to format all generated TypeScript files using your project's local prettier installation.
The @contractkit/prettier-plugin package formats .ck files themselves. Add it to your prettier config:
{
"plugins": ["@contractkit/prettier-plugin"]
}The @contractkit/vscode-extension extension provides:
- Syntax highlighting for
.ckfiles - Autocompletion for types, keywords, modifiers, and model references
- Hover information for built-in types and referenced models
- Cross-file model indexing
- Real-time diagnostics from the language server
Requires VS Code or Cursor 1.105.1+.
cd apps/vscode-extension
pnpm install
pnpm run vscode:installAll packages publish under the @contractkit npm scope.
contractkit/
apps/
cli/ # @contractkit/cli — contractkit binary (discovery, config, plugin orchestration)
vscode-extension/ # @contractkit/vscode-extension — VS Code / Cursor language support (LSP + TM grammar)
prettier-plugin/ # @contractkit/prettier-plugin — Prettier plugin for formatting .ck files
contracts/ # Example contract files
packages/
contractkit/ # @contractkit/core — parser, AST, semantics, plugin interface
src/
contractkit.ohm # Ohm PEG grammar (source of truth)
semantics.ts # Parse tree → AST
parser.ts # parseCk() entry point
ast.ts # AST type definitions
type-utils.ts # Type ref collection, topo sort, input-model graph
apply-options-defaults.ts # Merges options-level header globals into operations
validate-refs.ts # Cross-file type reference validation
validate-inheritance.ts # Multi-base inheritance validation
validate-operation.ts # Path parameter and operation validation
plugin.ts # ContractKitPlugin / PluginContext interfaces
plugin-typescript/ # @contractkit/plugin-typescript — Koa routers, TS SDK, Zod schemas, plain TS types
plugin-openapi/ # @contractkit/plugin-openapi — OpenAPI 3.0 YAML
plugin-markdown/ # @contractkit/plugin-markdown — Markdown API reference
plugin-bruno/ # @contractkit/plugin-bruno — Bruno REST collection
plugin-python/ # @contractkit/plugin-python — Python SDK (Pydantic v2 + httpx)
openapi-to-ck/ # @contractkit/openapi-to-ck — OpenAPI YAML → .ck file converter
config-typescript/ # Shared tsconfig base
config-eslint/ # Shared ESLint config