@ts-rest/core
Advanced tools
Comparing version 3.17.0 to 3.18.0
42
index.js
@@ -94,3 +94,3 @@ const isAppRoute = (obj) => { | ||
const defaultApi = async ({ path, method, headers, body, credentials, }) => { | ||
const tsRestFetchApi = async ({ path, method, headers, body, credentials, }) => { | ||
const result = await fetch(path, { method, headers, body, credentials }); | ||
@@ -118,4 +118,16 @@ const contentType = result.headers.get('content-type'); | ||
}; | ||
const fetchApi = (path, clientArgs, route, body) => { | ||
const apiFetcher = clientArgs.api || defaultApi; | ||
const normalizeHeaders = (headers) => { | ||
return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])); | ||
}; | ||
const fetchApi = ({ path, clientArgs, route, body, extraInputArgs, headers, }) => { | ||
const apiFetcher = clientArgs.api || tsRestFetchApi; | ||
const combinedHeaders = { | ||
...normalizeHeaders(clientArgs.baseHeaders), | ||
...normalizeHeaders(headers), | ||
}; | ||
Object.keys(combinedHeaders).forEach((key) => { | ||
if (combinedHeaders[key] === undefined) { | ||
delete combinedHeaders[key]; | ||
} | ||
}); | ||
if (route.method !== 'GET' && route.contentType === 'multipart/form-data') { | ||
@@ -126,6 +138,5 @@ return apiFetcher({ | ||
credentials: clientArgs.credentials, | ||
headers: { | ||
...clientArgs.baseHeaders, | ||
}, | ||
headers: combinedHeaders, | ||
body: body instanceof FormData ? body : createFormData(body), | ||
...extraInputArgs, | ||
}); | ||
@@ -138,6 +149,7 @@ } | ||
headers: { | ||
...clientArgs.baseHeaders, | ||
'Content-Type': 'application/json', | ||
'content-type': 'application/json', | ||
...combinedHeaders, | ||
}, | ||
body: body !== null && body !== undefined ? JSON.stringify(body) : undefined, | ||
...extraInputArgs, | ||
}); | ||
@@ -155,4 +167,12 @@ }; | ||
return async (inputArgs) => { | ||
const completeUrl = getCompleteUrl(inputArgs === null || inputArgs === void 0 ? void 0 : inputArgs.query, clientArgs.baseUrl, inputArgs === null || inputArgs === void 0 ? void 0 : inputArgs.params, route, !!clientArgs.jsonQuery); | ||
return await fetchApi(completeUrl, clientArgs, route, inputArgs === null || inputArgs === void 0 ? void 0 : inputArgs.body); | ||
const { query, params, body, headers, ...extraInputArgs } = inputArgs || {}; | ||
const completeUrl = getCompleteUrl(query, clientArgs.baseUrl, params, route, !!clientArgs.jsonQuery); | ||
return await fetchApi({ | ||
path: completeUrl, | ||
clientArgs, | ||
route, | ||
body, | ||
extraInputArgs, | ||
headers: headers || {}, | ||
}); | ||
}; | ||
@@ -231,2 +251,2 @@ }; | ||
export { ResponseValidationError, checkZodSchema, convertQueryParamsToUrlString, defaultApi, encodeQueryParams, encodeQueryParamsJson, fetchApi, getCompleteUrl, getRouteQuery, initClient, initContract, initTsRest, insertParamsIntoPath, isAppRoute, isAppRouteResponse, isZodObject, parseJsonQueryObject, validateResponse, zodErrorResponse }; | ||
export { ResponseValidationError, checkZodSchema, convertQueryParamsToUrlString, encodeQueryParams, encodeQueryParamsJson, fetchApi, getCompleteUrl, getRouteQuery, initClient, initContract, initTsRest, insertParamsIntoPath, isAppRoute, isAppRouteResponse, isZodObject, parseJsonQueryObject, tsRestFetchApi, validateResponse, zodErrorResponse }; |
{ | ||
"name": "@ts-rest/core", | ||
"version": "3.17.0", | ||
"version": "3.18.0", | ||
"description": "RPC-like experience over a regular REST API, with type safe server implementations ๐ช", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -28,16 +28,56 @@ # ts-rest | ||
ts-rest provides an RPC-like client side interface over your existing REST APIs, as well as allowing you define a _separate_ contract implementation rather than going for a 'implementation is the contract' approach, which is best suited for smaller or simpler APIs. | ||
ts-rest offers a simple way to define a contract for your API, which can be both consumed and implemented by your application, giving you end to end type safety without the hassle or code generation. | ||
If you have non typescript consumers, a public API, or maybe want to add type safety to your existing REST API? ts-rest is what you're looking for! | ||
### Features | ||
## Features | ||
- End to end type safety ๐ | ||
- Magic RPC-like API ๐ช | ||
- Tiny bundle size ๐ (1kb!) | ||
- RPC-like client side interface ๐ก | ||
- [Tiny bundle size ๐](https://bundlephobia.com/package/@ts-rest/core) (1kb!) | ||
- Well-tested and production ready โ | ||
- No Code Generation ๐โโ๏ธ | ||
- Zod support for body parsing ๐ฎโโ๏ธ | ||
- Zod support for runtime type checks ๐ฎโโ๏ธ | ||
- Full optional OpenAPI integration ๐ | ||
### Super Simple Example | ||
Easily define your API contract somewhere shared | ||
```typescript | ||
const contract = c.contract({ | ||
getPosts: { | ||
method: 'GET', | ||
path: '/posts', | ||
query: z.object({ | ||
skip: z.number(), | ||
take: z.number(), | ||
}), // <-- Zod schema | ||
responses: { | ||
200: c.response<Post[]>(), // <-- OR normal TS types | ||
}, | ||
}, | ||
}); | ||
``` | ||
Fulfil the contract on your sever, with a type-safe router: | ||
```typescript | ||
const router = s.router(contract, { | ||
getPost: async ({ params: { id } }) => { | ||
return { | ||
status: 200, | ||
body: prisma.post.findUnique({ where: { id } }), | ||
}; | ||
}, | ||
}); | ||
``` | ||
Consume the api on the client with a RPC-like interface: | ||
```typescript | ||
const result = await client.getPosts({ | ||
query: { skip: 0, take: 10 }, | ||
// ^-- Fully typed! | ||
}); | ||
``` | ||
## Quickstart | ||
@@ -44,0 +84,0 @@ |
import { AppRoute, AppRouteMutation, AppRouter } from './dsl'; | ||
import { ParamsFromUrl } from './paths'; | ||
import { HTTPStatusCode } from './status-codes'; | ||
import { AreAllPropertiesOptional, Merge, OptionalIfAllOptional, Without, ZodInferOrType, ZodInputOrType } from './type-utils'; | ||
type RecursiveProxyObj<T extends AppRouter> = { | ||
[TKey in keyof T]: T[TKey] extends AppRoute ? AppRouteFunction<T[TKey]> : T[TKey] extends AppRouter ? RecursiveProxyObj<T[TKey]> : never; | ||
import { AreAllPropertiesOptional, Merge, OptionalIfAllOptional, Prettify, Without, ZodInferOrType, ZodInputOrType } from './type-utils'; | ||
type RecursiveProxyObj<T extends AppRouter, TClientArgs extends ClientArgs> = { | ||
[TKey in keyof T]: T[TKey] extends AppRoute ? AppRouteFunction<T[TKey], TClientArgs> : T[TKey] extends AppRouter ? RecursiveProxyObj<T[TKey], TClientArgs> : never; | ||
}; | ||
@@ -18,8 +18,18 @@ type AppRouteMutationType<T> = ZodInputOrType<T>; | ||
type AppRouteBodyOrFormData<T extends AppRouteMutation> = T['contentType'] extends 'multipart/form-data' ? FormData | AppRouteMutationType<T['body']> : AppRouteMutationType<T['body']>; | ||
interface DataReturnArgsBase<TRoute extends AppRoute> { | ||
/** | ||
* Extract any extra parameters from the client args | ||
*/ | ||
export type ExtractExtraParametersFromClientArgs<TClientArgs extends ClientArgs> = TClientArgs['api'] extends ApiFetcher ? Omit<Parameters<TClientArgs['api']>[0], keyof Parameters<ApiFetcher>[0]> : {}; | ||
type DataReturnArgsBase<TRoute extends AppRoute, TClientArgs extends ClientArgs> = { | ||
body: TRoute extends AppRouteMutation ? AppRouteBodyOrFormData<TRoute> : never; | ||
params: PathParamsFromUrl<TRoute>; | ||
query: 'query' extends keyof TRoute ? AppRouteMutationType<TRoute['query']> : never; | ||
} | ||
type DataReturnArgs<TRoute extends AppRoute> = OptionalIfAllOptional<DataReturnArgsBase<TRoute>>; | ||
/** | ||
* Additional headers to send with the request, merged over baseHeaders, | ||
* | ||
* Unset a header by setting it to undefined | ||
*/ | ||
headers?: Record<string, string>; | ||
} & ExtractExtraParametersFromClientArgs<TClientArgs>; | ||
type DataReturnArgs<TRoute extends AppRoute, TClientArgs extends ClientArgs> = OptionalIfAllOptional<DataReturnArgsBase<TRoute, TClientArgs>>; | ||
export type ApiRouteResponse<T> = { | ||
@@ -37,3 +47,3 @@ [K in keyof T]: { | ||
*/ | ||
export type AppRouteFunction<TRoute extends AppRoute> = AreAllPropertiesOptional<Without<DataReturnArgs<TRoute>, never>> extends true ? (args?: Without<DataReturnArgs<TRoute>, never>) => Promise<ApiRouteResponse<TRoute['responses']>> : (args: Without<DataReturnArgs<TRoute>, never>) => Promise<ApiRouteResponse<TRoute['responses']>>; | ||
export type AppRouteFunction<TRoute extends AppRoute, TClientArgs extends ClientArgs> = AreAllPropertiesOptional<Without<DataReturnArgs<TRoute, TClientArgs>, never>> extends true ? (args?: Prettify<Without<DataReturnArgs<TRoute, TClientArgs>, never>>) => Promise<Prettify<ApiRouteResponse<TRoute['responses']>>> : (args: Prettify<Without<DataReturnArgs<TRoute, TClientArgs>, never>>) => Promise<Prettify<ApiRouteResponse<TRoute['responses']>>>; | ||
export interface ClientArgs { | ||
@@ -46,3 +56,3 @@ baseUrl: string; | ||
} | ||
type ApiFetcher = (args: { | ||
export type ApiFetcherArgs = { | ||
path: string; | ||
@@ -53,18 +63,36 @@ method: string; | ||
credentials?: RequestCredentials; | ||
}) => Promise<{ | ||
}; | ||
export type ApiFetcher = (args: ApiFetcherArgs) => Promise<{ | ||
status: number; | ||
body: unknown; | ||
}>; | ||
export declare const defaultApi: ApiFetcher; | ||
export declare const fetchApi: (path: string, clientArgs: ClientArgs, route: AppRoute, body: unknown) => Promise<{ | ||
/** | ||
* Default fetch api implementation: | ||
* | ||
* Can be used as a reference for implementing your own fetcher, | ||
* or used in the "api" field of ClientArgs to allow you to hook | ||
* into the request to run custom logic | ||
*/ | ||
export declare const tsRestFetchApi: ApiFetcher; | ||
export declare const fetchApi: ({ path, clientArgs, route, body, extraInputArgs, headers, }: { | ||
path: string; | ||
clientArgs: ClientArgs; | ||
route: AppRoute; | ||
body: unknown; | ||
extraInputArgs: Record<string, unknown>; | ||
headers: Record<string, string | undefined>; | ||
}) => Promise<{ | ||
status: number; | ||
body: unknown; | ||
}>; | ||
/** | ||
* @hidden | ||
*/ | ||
export declare const getCompleteUrl: (query: unknown, baseUrl: string, params: unknown, route: AppRoute, jsonQuery: boolean) => string; | ||
export declare const getRouteQuery: <TAppRoute extends AppRoute>(route: TAppRoute, clientArgs: ClientArgs) => (inputArgs?: DataReturnArgs<any>) => Promise<{ | ||
export declare const getRouteQuery: <TAppRoute extends AppRoute>(route: TAppRoute, clientArgs: ClientArgs) => (inputArgs?: DataReturnArgs<any, ClientArgs>) => Promise<{ | ||
status: number; | ||
body: unknown; | ||
}>; | ||
export type InitClientReturn<T extends AppRouter> = RecursiveProxyObj<T>; | ||
export declare const initClient: <T extends AppRouter>(router: T, args: ClientArgs) => RecursiveProxyObj<T>; | ||
export type InitClientReturn<T extends AppRouter, TClientArgs extends ClientArgs> = RecursiveProxyObj<T, TClientArgs>; | ||
export declare const initClient: <T extends AppRouter, TClientArgs extends ClientArgs>(router: T, args: TClientArgs) => RecursiveProxyObj<T, TClientArgs>; | ||
export {}; |
@@ -33,2 +33,5 @@ import { z } from 'zod'; | ||
}[keyof T]>; | ||
export type Prettify<T> = { | ||
[K in keyof T]: T[K]; | ||
} & {}; | ||
export {}; |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
39729
16
838
146