Getting Started
Get up and running with ts-contract quickly.
Install
pnpm add @ts-contract/core @ts-contract/pluginsnpm install @ts-contract/core @ts-contract/pluginsyarn add @ts-contract/core @ts-contract/pluginsbun add @ts-contract/core @ts-contract/pluginsDefine your contract
Use your favorite schema library to define your contract.
import { createContract } from '@ts-contract/core';
import { z } from 'zod';
const contract = createContract({
getUser: {
method: 'GET',
path: '/users/:id',
pathParams: z.object({ id: z.string() }),
responses: {
200: z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
}),
404: z.object({ message: z.string() }),
},
},
});import { createContract } from '@ts-contract/core';
import * as v from 'valibot';
const contract = createContract({
getUser: {
method: 'GET',
path: '/users/:id',
pathParams: v.object({ id: v.string() }),
responses: {
200: v.object({
id: v.string(),
name: v.string(),
email: v.pipe(v.string(), v.email()),
}),
404: v.object({ message: v.string() }),
},
},
});import { createContract } from '@ts-contract/core';
import { type } from 'arktype';
const contract = createContract({
getUser: {
method: 'GET',
path: '/users/:id',
pathParams: type({ id: 'string' }),
responses: {
200: type({
id: 'string',
name: 'string',
email: 'string.email',
}),
404: type({ message: 'string' }),
},
},
});Initialize your contract and add plugins
Once you've defined your contract, use initContract to create a builder and compose plugins with .use(). Call .build() to produce a fully enhanced contract.
import { } from '@ts-contract/core';
import { , } from '@ts-contract/plugins';
import { } from './contract';
const = ()
.()
.()
.();
// Build a type-safe URL
const = ..({ : '123' });
// => "/users/123"
// Validate incoming data against your schema
const = ..(200, { : '1', : 'Alice', : 'alice@example.com' });Type inference helpers
Use the built-in type helpers to extract types from your contract routes.
// Infer path parameters
type = <typeof .>;
// Infer a specific response body by status code
type = <typeof ., 200>;
// Infer all responses as a discriminated union
type = <typeof .>;
// Infer all arguments (path, query, body, headers) merged
type = <typeof .>;Fulfill your contract on the server
ts-contract doesn't own your server integration — you use the inference types to wire things up yourself.
import { type InferPathParams, type InferResponseBody } from '@ts-contract/core';
import { Hono } from 'hono';
import { contract } from './contract';
type Params = InferPathParams<typeof contract.getUser>;
// => { id: string }
type UserResponse = InferResponseBody<typeof contract.getUser, 200>;
// => { id: string; name: string; email: string }
const app = new Hono();
app.get('/users/:id', (c) => {
const { id } = c.req.param() as Params;
const user: UserResponse = {
id,
name: 'Alice',
email: 'alice@example.com',
};
return c.json(user);
});import { type InferPathParams, type InferResponseBody } from '@ts-contract/core';
import express from 'express';
import { contract } from './contract';
type Params = InferPathParams<typeof contract.getUser>;
// => { id: string }
type UserResponse = InferResponseBody<typeof contract.getUser, 200>;
// => { id: string; name: string; email: string }
const app = express();
app.get('/users/:id', (req, res) => {
const { id } = req.params as Params;
const user: UserResponse = {
id,
name: 'Alice',
email: 'alice@example.com',
};
res.json(user);
});import { type InferPathParams, type InferResponseBody } from '@ts-contract/core';
import Fastify from 'fastify';
import { contract } from './contract';
type Params = InferPathParams<typeof contract.getUser>;
// => { id: string }
type UserResponse = InferResponseBody<typeof contract.getUser, 200>;
// => { id: string; name: string; email: string }
const app = Fastify();
app.get<{ Params: Params }>('/users/:id', async (request) => {
const { id } = request.params;
const user: UserResponse = {
id,
name: 'Alice',
email: 'alice@example.com',
};
return user;
});Use in your client code
Combine your contract with React Query for fully type-safe data fetching.
import { useQuery } from '@tanstack/react-query';
import { type InferPathParams, type InferResponseBody } from '@ts-contract/core';
import { contract } from './contract';
import { api } from './api';
type Params = InferPathParams<typeof contract.getUser>;
// => { id: string }
type User = InferResponseBody<typeof contract.getUser, 200>;
// => { id: string; name: string; email: string }
export function useUser(id: string) {
return useQuery<User>({
queryKey: ['user', id],
queryFn: async () => {
const url = api.getUser.buildPath({ id });
const res = await fetch(url);
const data = await res.json();
// Validate the response against your schema
return api.getUser.validateResponse(200, data);
},
});
}import { useUser } from './use-user';
function UserProfile({ id }: { id: string }) {
const { data: user, isLoading, error } = useUser(id);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}