RPC-like client, contract, and server implementation for a pure REST API

Overview

ts-rest

RPC-like client and server helpers for a magical end to end typed experience

langue typescript npm

Introduction

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.

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

  • End to end type safety 🛟
  • Magic RPC-like API 🪄
  • Tiny bundle size 🌟 (1kb!)
  • Well-tested and production ready
  • No Code Generation 🏃‍♀️
  • Zod support for body parsing 👮‍♀️
  • Full optional OpenAPI integration 📝

Quickstart

Install the core package

yarn add @ts-rest/core
# Optional react-query integration
yarn add @ts-rest/react-query
# Pick your backend
yarn add @ts-rest/nest @ts-rest/express
# For automatic server OpenAPI gen
yarn add @ts-rest/open-api

Create a contract, implement it on your server then consume it in your client. Incrementally adopt, trial it with your team, then get shipping faster.

👉 Read more on the official Quickstart Guide 👈

Comments
  • Improved query string encoding and decoding

    Improved query string encoding and decoding

    Following up on https://github.com/ts-rest/ts-rest/pull/80, I've transitioned the nest library to use req.query and req.params rather than parsing them from the URL. This now allows the usage of arrays and nested objects in query params. I've made sure that this use-case is also covered in the clients by changing convertQueryParamsToUrlString to use qs.stringify which is the same library used internally by express.js.

    Also tweaked the Nest implementation such that the all contracts' AppRoutes are added to their respective methods' metadata. This happens only once during runtime at startup, and therefore does need to be added to each request object. This allows us to maintain a single instance of ApiRouteInterceptor in the IoC container across all routes.

    I've also changed how the status and response body are returned from ApiRouteInterceptor. Throwing an HttpException meant that no other interceptors could have been used since the thrown exception goes straight to Nest's base exception filter.

    Added some tests in the example-nest app.

    opened by Gabrola 24
  • initQueryClient seems invalid in @ts-rest/react-query

    initQueryClient seems invalid in @ts-rest/react-query

    Hey @oliverbutler First thanks for this amazing tool! I ran an error while using @ts-rest/react-query : No QueryClient set, use QueryClientProvider to set one (nx, cra/nest)

    This problem happens only with the compiled package, I have no error when I copy the client from your monorepo and then create my own lib.

    opened by lekinano 15
  • chore: fix ESM support

    chore: fix ESM support

    Context

    Hope to officially close #66 without workarounds. To be clear, I do not want to merge these changes just yet, but I want to get at least the conversation going, and a PR started so that we can implement @ecyrbe 's suggestions or our own fix.

    Background

    On my NextJS webpack build, and several others, we observe the following error:

    Server Error
    Error: No QueryClient set, use QueryClientProvider to set one
    
    This error happened while generating the page. Any console logs will be displayed in the terminal window.
    

    Implementing this workaround no longer works for me (for some reason):

        if (options.isServer) {
          config.externals = ["@tanstack/react-query", ...config.externals];
        }
        const reactQuery = path.resolve(require.resolve("@tanstack/react-query"));
        console.log(reactQuery, 'converted ts-rest imports for react-query')
        config.resolve.alias["@tanstack/react-query"] = reactQuery;
        return config;
      },
    

    According to #66 and @ecyrbe, the likely culprit is that exports are not being correctly interpreted by webpack. See: https://github.com/ecyrbe/zodios/issues/191#issuecomment-1288101919.

    I released a temporary test version of @ts-rest/react-query here: https://www.npmjs.com/package/michaelangrivera-ts-rest-react-query, and I still see the same issue while implementing that suggestion.

    Interestingly, though, the following works:

    `const client = require('michaelangrivera-ts-rest-react-query')`
    

    I guess this is to be expected, though, since ESM is failing and not common modules.

    This is what the build looks like after nx build for that library:

    {
      "name": "michaelangrivera-ts-rest-react-query",
      "version": "3.6.6",
      "description": "react-query client integration for @ts-rest",
      "license": "MIT",
      "main": "./index.cjs",
      "module": "./index.js",
      "typings": "index.d.ts",
      "exports": {
        ".": {
          "types": "./index.d.ts",
          "import": "./index.js",
          "require": "./index.cjs"
        },
        "./package.json": "./package.json"
      },
    ....
      "peerDependencies": {
        "react": "16.x.x || 17.x.x || 18.x.x",
        "zod": "3.x.x",
        "@tanstack/react-query": "4.x.x",
        "@ts-rest/core": "3.6.0"
      },
      "type": "module",
      "types": "./index.d.ts",
      "dependencies": {}
    }
    

    I tried the following changes (I published them, re-installed, and cleared .next cache):

    • "generateExportsField": true, no luck
    • only using esm under the format configuration, no luck

    I'm wondering if changing type: to module in package.json would work any magic.

    Let me know if you have any suggestions, and I can implement, test them, and then update this PR @oliverbutler @ecyrbe.

    I could also be totally off, and this is actual a client instance issue with React Query and the way it's be initialized.

    This whole effort may be futile as Next is moving away from webpack, but nonetheless, it may still be good to support webpack for legacy apps and people using webpack without Next.

    opened by michaelangrivera 12
  • Unnecessarily requiring args for a GET request

    Unnecessarily requiring args for a GET request

    First of all, congrats on building such a cool library! 🙌

    It seems I've stumbled upon an unwanted behaviour, when calling a client router method that simply does a GET request without any required parameters:

    I might be missing something, but it seems that the root cause is in the DataReturn type that's being applied where the args parameter is never optional:

    // ts-rest/libs/ts-rest/core/src/lib/client.ts
    
    /**
     * Returned from a mutation or query call
     */
    export type DataReturn<TRoute extends AppRoute> = (
      args: Without<DataReturnArgs<TRoute>, never>
    ) => Promise<ApiRouteResponse<TRoute['responses']>>;
    

    1 - Could we make args optional for "query calls" where there aren't any required path parameters?


    Also, if the query property is specified on the contract:

    const c = initContract();
    
    export const contract = c.router({
      getMembers: {
        method: 'GET',
        path: 'members',
        responses: {
          200: c.response<{ members: Member[]; total: number }>(),
        },
        query: z.object({
          take: z.string().transform(Number).optional(),
          skip: z.string().transform(Number).optional(),
          search: z.string().optional(),
        }),
        summary: 'Get all members',
      },
    });
    

    It seems to become required on the client:

    2 - Could we make query params optional?

    enhancement 
    opened by miguellteixeira 10
  • ❓ Doc improvement

    ❓ Doc improvement

    Hi @oliverbutler

    Thanks for this very interresting project.

    Have you an example to integrate @ts-rest/open-api with @ts-rest/express ?

    EDIT:

    Below my local implementation

    import { contract } from "./contract";
    import { generateOpenApi } from "@ts-rest/open-api";
    import { serve, setup } from "swagger-ui-express";
    
    const openapi = generateOpenApi(contract, {
      info: { title: "Play API", version: "0.1" },
    });
    
    const apiDocs = Router();
    
    apiDocs.use(serve);
    apiDocs.get("/", setup(openapi));
    
    app.use("/api-docs", apiDocs);
    
    createExpressEndpoints(contract, router, app);
    
    documentation 
    opened by ghoullier 10
  • fix: don't use $ref in OpenAPI

    fix: don't use $ref in OpenAPI

    If the same zod object is used multiple times in the same contract object (query, body or response), it will use a $ref to refer to it's first occurrence. For the example in test code, it would have generated to following JSON schema.

    "comments": {
      "anyOf": [
    	{
    	  "items": {
    		"additionalProperties": false,
    		"properties": {
    		  "id": {
    			"type": "number"
    		  },
    		  "title": {
    			"type": "string"
    		  }
    		},
    		"required": [
    		  "id",
    		  "title"
    		],
    		"type": "object"
    	  },
    	  "type": "array"
    	},
    	{
    	  "items": {
    		"additionalProperties": false,
    		"properties": {
    		  "author": {
    			"type": "string"
    		  },
    		  "id": {
    			"$ref": "#/definitions/zodObject/properties/comments/anyOf/0/items/properties/id"
    		  },
    		  "title": {
    			"$ref": "#/definitions/zodObject/properties/comments/anyOf/0/items/properties/title"
    		  }
    		},
    		"required": [
    		  "id",
    		  "title",
    		  "author"
    		],
    		"type": "object"
    	  },
    	  "type": "array"
    	}
      ]
    }
    

    Since we aren't actually bundling any definitions, these references don't resolve and we end up with an error like this.

    Screenshot 2022-12-16 210549

    We also fix this by naming and bundling all definitions generated, but it'll be so clunky. We'd end up with definitions named something like commentsGetPostCommentsResponses200

    opened by Gabrola 8
  • @Api decorator not working properly with nest versioning

    @Api decorator not working properly with nest versioning

    Hey guys! I'm having some issues when creating an endpoint on nestjs + ts-rest that receives a param and has versioning configured by nest have you guys ever faced these issues?

    Example:

    Contract route

     getIndustryTag: {
        method: 'GET',
        path: `/industry-tags/:uid`,
        responses: {
          200: ResponseIndustryTagModel.nullable(),
        },
        summary: 'Get an industry tag',
      },
    

    Controller

    @Controller({
      version: '1',
    })
    export class IndustryTagsController {
      constructor(private readonly industryTagsService: IndustryTagsService) {}
    
      @Api(server.route.getIndustryTag)
      async findOne(
        @ApiDecorator() { params: { uid } }: RouteShape['getIndustryTag']
      ) {  
            ....
        }
    

    The end route for this would be http://localhost:3000/v1/industry-tags/uid-123 but since the contract route doesn't know about the versioning that was applied, I receive the string industry-tags has the param instead of the uid-123

    Even if I apply the v1 to the contract path it will become v1/v1/industry-tags so it wouldn't help either

    The only solution I can see working is managing the versioning inside the contract only and letting go of the nestjs versioning system, but I wanted to know if there are other options for this and if this is a known issue

    Thanks!

    enhancement help wanted 
    opened by PedroFonseca17 8
  • feat: update custom api docs

    feat: update custom api docs

    Hi!

    Figured I'd try to help out and contribute back.

    It doesn't seem baseUrl is accessible in the api method. It may be missing as a type, or not a feature yet. I can look into that and open a PR.

    I moved all the custom stuff to a Custom Client page to make it more apparent since I feel there will be many other people wanting to take advantage of this.

    Thank you, and let me know what you think!

    opened by michaelangrivera 7
  • fix: use z.input for body and query types on clients

    fix: use z.input for body and query types on clients

    For cases where zod input and output types differ such as when using .default() or .transform(), we need to use the input type on the client-side.

    const foo = z.object({ bar: z.string().default('bar') });
    type input = z.input<typeof foo>; // { bar?: string | undefined }
    type output = z.infer<typeof foo>; // { bar: string }
    
    opened by Gabrola 6
  • @ApiDecorator retrieves wrong params in Nest

    @ApiDecorator retrieves wrong params in Nest

    Hey, I've been using ts-rest for a few days and find it extremely well made! But I have quite a big issue.

    @ApiDecorator is not working properly.

    When hitting /media/:id with like /media/some-id it returns media instead of some-id

    // .............
    
    const m = initContract();
    
    const mediaContract = m.router({
    // ..........
      getMedia: {
        method: 'GET',
        path: '/:id',
        responses: { 200: m.response<any>(), },
        summary: 'get a media',
      },
    })
    
    const c = initContract();
    
    export const contract = c.router({
     //..........
      media: mediaContract,
    });
    
    const s = initRouteContract(contract.media);
    type ControllerShape = typeof s.controllerShape;
    type RouteShape = typeof s.routeShapes;
    
    @Controller('media')
    export class MediaController implements ControllerShape {
      @Api(s.route.getMedia)
      async getMedia(@ApiDecorator() { params: { id } }: RouteShape['getMedia']) { 
        return id; // returns "media" instead of the ID
      }
    }
    

    Please provide an hotfix, I can't use the library anymore for now... because of this bug

    EDIT : WORKAROUND FOR NOW : use the native @Param from NestJS :

    @Api(s.route.getMedia)
    // async getMedia(@ApiDecorator() { params: { id } }: RouteShape['getMedia']) {
    async getMedia(@Param('id') id: string) {
      return id; // returns "some-id" correctly
    }
    
    bug 
    opened by franckdsf 6
  • fix: revert react-query exports in package.json

    fix: revert react-query exports in package.json

    @oliverbutler Sorry about this, I removed the exports from the package.json in the last PR and it seems like it broke something and I have no idea why, but you probably had it in there for the some valid reason. Without the exports in package.json I get this error in Next.js

    No QueryClient set, use QueryClientProvider to set one

    From some quick research it seems like this happens when different react-query versions are being used, but I could not observe that in my case so I dunno honestly 🤷‍♂️

    opened by Gabrola 5
  • feat: add response shape feature, update docs

    feat: add response shape feature, update docs

    Per discussion on Discord, this feature allows users to extract response types from a given Contract. This is particularly useful for backend services where you want to avoid implementing an interface.

    I aimed to make this as non-intrusive as possible! All tests pass. Please read the modified Docs to get a feel for the changes/additions, and then you can look at client.ts to see the entry point of the implementation.

    Here's a screenshot of how it works:

    Screenshot 2022-12-23 at 1 26 32 PM
    opened by michaelangrivera 5
  • Document @ts-rest/open-api

    Document @ts-rest/open-api

    This is potentially a huge aspect of ts-rest, it would be awesome to document, possibly with some interactive examples how the lib works.

    @ghoullier I'm happy to have you help out with this, mind that I'm not particularly considering any changes to the @ts-rest/open-api lib to be breaking until we release it on the docs 🤔

    If you don't have time that's completely OK I can put it on the backlog!

    documentation help wanted good first issue 
    opened by oliverbutler 0
  • Release the solid-query adapter fully

    Release the solid-query adapter fully

    @ts-rest/solid-query is 95% working, the only broken part is that the args attribute should be a callback so that solid js can react to changes to params/query params etc.

    Screenshot 2022-12-18 at 13 52 50
    • Fix () -> args
    • Fix data return type to make sure it's { status, body } just like the rest of ts-rest
    documentation enhancement 
    opened by oliverbutler 3
  • Document *how* ts-rest works

    Document *how* ts-rest works

    I want to better explain the mental model, with some diagrams.

    Contract initContract from @ts-rest/core

    • Defined only on the front end, for an uncontrollable API
    • Defined in a shared lib, used by both front and backend
    • Defined by a shared npm module, for an internal or external API

    ^ To clarify and advertise that it isn't a full stack, all-or-nothing tech, it can be incrementally adopted and has no backend requirements.

    Base client initClient from @ts-rest/core

    • Is a simple wrapper around fetch, nothing more, it has a type-safe wrapper around it but the implementation is SERIOUSLY tiny
    • Can be used for any framework, not restricted to @tanstack adapters

    ^ To clarify that you can use any framework, no limits. (#71 )

    Other libs

    • Adapters for first-class support with other libs, NOT REQUIRED

    ^ To clarify that you can use ts-rest even without a supported adapter e.g. Vue (#71 )

    documentation 
    opened by oliverbutler 1
  • Unable to create nested contract router with loosing path narrowing

    Unable to create nested contract router with loosing path narrowing

    this works

    const claims = c.router({
      getClaimsForApproval: {
        method: 'GET',
        path: `/partner/:partnerId/claims`,
        responses: { 200: c.response<{ id: string }[]>() },
        query: z.object({
          take: z.number().optional(),
          skip: z.number().optional(),
        }),
      }
    });
    
    export const claimServicePartnerApi = c.router({
      claims,
    });
    
    

    this doesn't work "path" becomes string

    export const claimServicePartnerApi = c.router({
      claim: {
        getClaimsForApproval: {
          method: 'GET',
          path: `/partner/:partnerId/claims`,
          responses: { 200: c.response<{ id: string }[]>() },
          query: z.object({
            take: z.number().optional(),
            skip: z.number().optional(),
          }),
        }
     }
    });
    
    

    I think it's due to how I'm naïvely applying the Narrow helper, it probably should recursively go though the object...

    bug 
    opened by oliverbutler 1
Releases(@ts-rest/[email protected])
Owner
tREST
tREST
Patronum: Ethereum RPC proxy that verifies RPC responses against given trusted block hashes

Patronum Ethereum RPC proxy that verifies RPC responses against given trusted block hashes. Currently, most of the DAPPs and Wallets interact with Eth

null 14 Dec 7, 2022
Grupprojekt för kurserna 'Javascript med Ramverk' och 'Agil Utveckling'

JavaScript-med-Ramverk-Laboration-3 Grupprojektet för kurserna Javascript med Ramverk och Agil Utveckling. Utvecklingsguide För information om hur utv

Svante Jonsson IT-Högskolan 3 May 18, 2022
Hemsida för personer i Sverige som kan och vill erbjuda boende till människor på flykt

Getting Started with Create React App This project was bootstrapped with Create React App. Available Scripts In the project directory, you can run: np

null 4 May 3, 2022
Kurs-repo för kursen Webbserver och Databaser

Webbserver och databaser This repository is meant for CME students to access exercises and codealongs that happen throughout the course. I hope you wi

null 14 Jan 3, 2023
This is a vanilla Node.js rest API created to show that it is possible to create a rest API using only vanilla Node.js

This is a vanilla Node.js rest API created to show that it is possible to create a rest API using only vanilla Node.js. But in most cases, I would recommend you to use something like Express in a production project for productivity purposes.

Eduardo Dantas 7 Jul 19, 2022
Basic Implementation of a Contract Wallet in Solidity. The owner can transfer Ether/ERC20 and execute transactions via low-level calls.

Contract Wallet Basic Implementation of a Contract Wallet in Solidity. The owner can transfer Ether/ERC20 and execute transactions via low-level calls

Junho Yeo 3 Jun 18, 2022
Monolithic repo for api server, image server, web server

Onsecondary Market Deployed at https://market.onsecondary.com Monolithic repo for api server, image server, web server TODO -use a script to cull expi

Admazzola 2 Jan 11, 2022
Deno client library for Bitwarden CLI's local REST API.

Bweno Think outside the bun ?? Bweno is a client library for Bitwarden CLI's local REST API. Pronounced as Spanish 'bueno', meaning 'good'. Requiremen

Wyatt Goettsch 3 May 26, 2022
Generate type definitions compatible with @kintone/rest-api-client

kintone-form-model-generator Generate type definitions compatible with @kintone/rest-api-client Prerequirements Node.js (>=12) Install # Install npm i

Yuuki Takahashi 5 Dec 15, 2022
An ERC-721 like NFT contract with Plutus scripts and Lucid as off-chain framework

Gatsby minimal TypeScript starter ?? Quick start Create a Gatsby site. Use the Gatsby CLI to create a new site, specifying the minimal TypeScript star

Berry 10 Sep 23, 2022
an open-source package to make it easy and simple to work with RabbitMQ's RPC ( Remote Procedure Call )

RabbitMQ Easy RPC (Remote Procedure Call ) The Node.js's RabbitMQ Easy RPC Library rabbitmq-easy-RPC is an easy to use npm package for rabbitMQ's RPC

Ali Amjad 4 Sep 22, 2022
A MITM cache between RPCs and a a dAPP. Useful to allow for better performance on a public RPC node

better-cosmos-rpcs A cheaper way to allow for public RPCs as a service WITHOUT scaling issues. No need to rate limit either. How it is done: User GET

Reece Williams 3 Nov 19, 2022
An interactive Bitcoin tutorial for orange-pilled beginners. Illustrates technical Bitcoin concepts using JavaScript and some Bitcoin Core RPC commands. Programming experience is helpful, but not required.

Try Bitcoin Try Bitcoin is an interactive Bitcoin tutorial inspired by and forked from Try Regex, which is inspired by Try Ruby and Try Haskell. It il

Stacie Waleyko 33 Nov 25, 2022
Unofficial API client for the Tidbyt API. Use this client to control Tidbyt devices and integrate with other services.

Tidbyt Client for Node.js Unofficial API client for the Tidbyt API. Use this client to control Tidbyt devices and integrate with other services. Insta

Nicholas Penree 19 Dec 17, 2022
Collection of JSON-RPC APIs provided by Ethereum 1.0 clients

Ethereum JSON-RPC Specification View the spec The Ethereum JSON-RPC is a collection of methods that all clients implement. This interface allows downs

null 557 Jan 8, 2023
Run RPC over a MessagePort object from a Worker thread (or WebWorker)

thread-rpc Run RPC over a MessagePort object from a Worker thread (or WebWorker) npm install thread-rpc Usage First in the parent thread const Thread

Mathias Buus 9 May 31, 2022
my ethereum RPC node setup & notes

UBUNTU RPC (scaffold-rpc) sudo add-apt-repository -y ppa:ethereum/ethereum sudo apt-get update sudo apt-get install ethereum = created geth script = g

Austin Griffith 10 Jul 4, 2022
⚡🚀 Call multiple view functions, from multiple Smart Contracts, in a single RPC query!

ethers-multicall ⚡ ?? Call multiple view functions, from multiple Smart Contracts, in a single RPC query! Querying an RPC endpoint can be very costly

Morpho Labs 20 Dec 30, 2022
A work-in-progress HTML sanitizer that strives for: performance like window.Sanitizer, readiness like DOMPurify, and ability to run in a WebWorker like neither of those.

Amuchina A work-in-progress HTML sanitizer that strives for: performance like window.Sanitizer, readiness like DOMPurify, and ability to run in a WebW

Fabio Spampinato 9 Sep 17, 2022