GithubHelp home page GithubHelp logo

ts-rest / ts-rest Goto Github PK

View Code? Open in Web Editor NEW
2.0K 2.0K 86.0 7.65 MB

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

Home Page: https://ts-rest.com

License: MIT License

TypeScript 87.27% JavaScript 2.11% CSS 1.95% HTML 0.18% MDX 8.05% Vue 0.44%
api fastify nestjs nextjs openapi react react-query rest rpc solid typescript

ts-rest's Introduction

ts-rest

Incrementally adoptable RPC-like client and server helpers for a magical end to end typed experience πŸͺ„

GitHub Repo stars License Bundle Size

Join us on Discord for help, feedback, and discussions!


Discord Shield

Introduction

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.

Features

  • End-to-end type safety πŸ›Ÿ
  • RPC-like client side API ⚑️
  • Small Bundle Size πŸ“‰
  • No Code Generation πŸƒβ€β™€οΈ
  • Zod support for runtime validation πŸ”’
  • Full optional OpenAPI integration πŸ“

πŸ‘‰ Start reading the official Quickstart Guide πŸ‘ˆ

Super Simple Example

Easily define your API contract somewhere shared

const contract = c.router({
  getPosts: {
    method: 'GET',
    path: '/posts',
    query: z.object({
      skip: z.number(),
      take: z.number(),
    }), // <-- Zod schema
    responses: {
      200: c.type<Post[]>(), // <-- OR normal TS types
    },
    headers: z.object({
      'x-pagination-page': z.coerce.number().optional(),
    }),
  },
});

Fulfill the contract on your server, with a type-safe router:

const router = s.router(contract, {
  getPosts: async ({ params: { id } }) => {
    return {
      status: 200,
      body: prisma.post.findUnique({ where: { id } }),
    };
  },
});

Consume the api on the client with a RPC-like interface:

const result = await client.getPosts({
  headers: { 'x-pagination-page': 1 },
  query: { skip: 0, take: 10 },
  // ^-- Fully typed!
});

Quickstart

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.

πŸ‘‰ Start reading the official Quickstart Guide πŸ‘ˆ

Contributors ✨

MASSIVE Thanks to all of these wonderful people (emoji key), who have helped make ts-rest possible:

Youssef Gaber
Youssef Gaber

πŸ’» πŸ€” ⚠️
Per Hermansson
Per Hermansson

πŸ“– πŸ’»
GrΓ©gory Houllier
GrΓ©gory Houllier

πŸ“–
Michael Angelo
Michael Angelo

πŸ“–
Pieter Venter
Pieter Venter

πŸ“–
Rifaldhi AW
Rifaldhi AW

πŸ“–
Jonathan White
Jonathan White

πŸ’» πŸ“–
Max Brosnahan
Max Brosnahan

πŸ’» πŸ€”
Oliver Butler
Oliver Butler

πŸ’» πŸ€” πŸ“– πŸš‡ 🚧
Adrian Barylski
Adrian Barylski

πŸ’» πŸ“– ⚠️
Neil A. Dobson
Neil A. Dobson

πŸ’»
Eric Do
Eric Do

πŸ“–
Ben
Ben

πŸ’» πŸ“– ⚠️
LW
LW

πŸ’» πŸ›
Andrew Vance
Andrew Vance

πŸ“–

Star History

Since our first commit in 2022 we've been growing steadily. We're proud of our progress and we're excited about the future.

Join us on Discord for help, feedback, and discussions!


Discord Shield

ts-rest's People

Contributors

allcontributors[bot] avatar cyrus-za avatar fruchtzwerg avatar gabrola avatar gingermusketeer avatar github-actions[bot] avatar hyphaene avatar icestorm avatar ihusemann avatar jwcode-uk avatar llllvvuu avatar lukemorales avatar masonpimentel avatar michaelangeloio avatar mtxr avatar nad-au avatar netiperher avatar oliverbutler avatar oscartbeaumont avatar qwici avatar reecefenwick avatar rifaldhiaw avatar robhung avatar romain-trotard avatar saenyakorn avatar scarf005 avatar seancassiere avatar sharkezz avatar suphon-t avatar toteto avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ts-rest's Issues

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 )

Error handling in Express server

Today when using the createExpressEndpoints it will wrap all route handlers in a

     try {
     ...
     } catch (e) {
      console.error(`[ts-rest] Error on ${method} ${schema.path}`, e);
      return res.status(500).send('Internal Server Error');
    }

making it difficult to use any kind of error handling middleware in Express.

Would it be an option to just remove this handling completely and simply let Express take care of forwarding the error to next handler/middleware?

[question] couldn't we let nestjs controller methods decide the return types?

Hello team,

Would it be possible for the NestJS integration to let the endpoints themselves define their return types?

We'd make it force a base return type shape containing the http status code like:

type TsRestResponseBase = { [Status in 200 | 201]: unknown } // this is just a silly example

@Controller()
export class PostController implements NestControllerInterface<typeof c> {
  constructor(private readonly postService: PostService) {}

  @TsRest(c.getPost)
  async getPost(@TsRestRequest() { params: { id } }: TsRestResponseBase {
    const post = await this.postService.getPost(id);

    if (!post) {
      return { status: 404 as const, body: null };
    }

    return { status: 200 as const, body: post };
  }
}

export const finalPostControllerRoutes = resolveNestJsController<PostController>(); // another silly example

And somehow the router/contract infer the final return type from that end point dinamically?

Or would this idea be worse than the actual behavior of statically forcing all the return types of controller methods thru the contract?

Thank you!

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...

Errors in README

Hey, thanks for the library!

Just wanted to point out that there are some errors in the README, I think you wanted to have tREST instead of tRPC:

  • in the title
  • 2 times in the introduction paragraph

Feature request - ArkType support

ArkType is a new validator that I am wanting to try out but with ts-rest I am locked in to zod. I saw in discord that there is some intention to decouple validator logic from zod because of other validators like TypeBox and Deepkit. I'm creating this feature request because that is something I'm interested in.

@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!

TypeError: Failed to execute 'text' on 'Response': body stream already read

Hi, first of all, congrats making this really good library !

I'm using ts-rest inside an NX Monorepo with NextJS & NestJS.

I got my two apps (generated form @nrwl/nest & @nrwl/next) and a lib for the contracts.

Here are my definitions :

// index.ts in shared/contracts/src (my library)
import { initContract } from '@ts-rest/core';

const c = initContract();

export const healthCheckContract = c.router({
  getHealthcheck: {
    method: 'GET',
    path: '/',
    responses: {
      200: c.response<string>(),
    },
    summary: 'Healthcheck API to check if the service is up and running',
  },
});
// app.controller.ts in NestJS app
import { Controller } from '@nestjs/common';
import {Api, initNestServer} from "@ts-rest/nest";
import {healthCheckContract} from "@ecommerce/contracts";

const s = initNestServer(healthCheckContract);
type ControllerShape = typeof s.controllerShape;
type ResponseShapes = typeof s.responseShapes;

@Controller()
export class AppController implements ControllerShape {
  @Api(s.route.getHealthcheck)
  async getHealthcheck(): Promise<ResponseShapes['getHealthcheck']> {
    return  {
      status: 200,
      body: 'Hello World!'
    };
  }
}
// _app.tsx in NextJS app (init queryClientProvider)
import { AppProps } from 'next/app';
import Head from 'next/head';
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {useState} from "react";

export default function CustomApp({ Component, pageProps }: AppProps) {
  const [queryClient] = useState(() => new QueryClient());

  return (
    <>
      <Head>
        <title>Welcome to web!</title>
      </Head>
      <main className="app">
        <QueryClientProvider client={queryClient}>
            <Component {...pageProps} />
        </QueryClientProvider>
      </main>
    </>
  );
}
// index.tsx in NextJS app (fetching data)
import {initQueryClient} from "@ts-rest/react-query";
import {healthCheckContract} from "@test/contracts";
import {useEffect} from "react";

export default function Index() {
  const client = initQueryClient(healthCheckContract, {
    baseUrl: 'http://localhost:3333/api',
    baseHeaders: {},
  });
  const {data} = client.getHealthcheck.useQuery(['healthcheck'])

  useEffect(() => {
    console.log('data', data)
  }, [data])

  return (
    <div>
      <h1>Welcome to web!</h1>
    </div>
  );
}

I can watch the query response, and from there, all good :

image

The console.log pops one time, and says "undefined"
Then this error appears in the console :

image

Coming from this part of code :

image

And, as I can see, in network tab, the query repeats itself again and again.

No type errors so I presume my code is correct but maybe there is something I miss ?
Thank you for your help.

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

Capability to send non-JSON responses

The express and next server-side implementations do not allow the request handlers to return anything other than JSON since we are returning the response using res.status(statusCode).json(body)

There is no way to prevent this behavior so we'd need to expose the response object to the request handler functions, in addition, we need to define a method to prevent the default behavior of calling .json on the response object.

Might need to re-visit how we can define these responses in the contract if possible.

This problem doesn't exist in the NestJS implementation since we do a res.status(...); return body; in the interceptor, and the handler can access the response object by injecting it using the @Res decorator. Will need to double check that calling res.status(...); does not cause problems in case the status and/or response body was already sent manually by the handler.

c.body() not working with express

The contract's body() escape mechanism for when there is no zod schema is not working for the router's body param when using express server.

See this example:

api:

export const apiBlog = c.router({
  createPost: {
    method: 'POST',
    path: '/posts',
    responses: {
      201: PostSchema,
    },
    body: c.body<Post>(),
    summary: 'Create a post',
  },
});

server:

const completedRouter = s.router(apiBlog, {
  createPost: async ({ body }) => {
    const post = await prisma.post.create({
      data: body,
    });
    return {
      status: 201,
      body: post,
    };
  },
});

results in

       TS2339: Property 'body' does not exist on type 'Without<{ params: undefined; query: never; body: never; headers: IncomingHttpHeaders; }, never>'.

possibly related to this definition in ts-rest-express.ts

      body: T['body'] extends ZodTypeAny ? z.infer<T['body']> : never;

Add type-safe headers

I've had a couple of problems where I forgot to pass headers to my APIs, we could add this to add another level of type safety to ts-rest.

Accessing Request object when using @ApiDecorator

Hello, I have been using @ts-rest with NestJS and enjoying it so far, however, documentation is sparse.

It's extremely common to need access to request headers and metadata in controllers, however, I don't see any way to achieve that when using @ts-rest. The @Api only returns the parsed params, query, and body, but the @Api decorator forces you to use @ApiDecorator and both of those require you to extend the ControllerShape of the Contract.

How can we access the full request in the controller when using @ts-rest with NestJS?

Could this be used for Event contracts

Hi there,

Thanks for this library - it's awesome!

I have played with the Nest.js Controller contracts and it's working like a charm. That's why I thought it could solve the same problem for other Nest.js based microservices e.g. on Redis Pub/Sub or communication via queues.

It seems to be possible by abusing the contract, ignore the method and use the path as the event name. As a next step you can use the Nest.js Controller contracts to validate the incoming events. The ugly part is that the events need to follow the { body: {}, params: {}... } spec.

Would it be possible (and in your interest) to build a contract feature for event based systems on ts-rest?

IE11 Support

I'm getting 'Proxy' is undefined in IE11.

Would Polyfiling Proxy be the only obstacle to IE11 usage? Is there any plan to support IE11?

At my company, we've switched from tRPC to ts-rest in the hope that we can use it in IE11.

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.

Passing Error object directly to HttpException is deprecated (NestJS)

Passing the Zod validation error directly to the BadRequestException constructor is now triggering this warning

DEPRECATED! Passing the error cause as the first argument to HttpException constructor is deprecated. You should use the "options" parameter instead: new HttpException("message", 400, { cause: new Error("Some Error") })

Custom Client automatically stringifies body and does not provide access to raw body object

Take the following example from our docs:

import { contract } from './some-contract';
import axios, { Method, AxiosError, AxiosResponse, isAxiosError } from 'axios';

export class SampleAPI {
  token: string;
  constructor(params: { token: string }) {
    this.token = params.token;
    this.baseUrl = 'http://localhost:3333/api';
  }
  client = () => {
    return initClient(contract, {
      baseUrl: this.baseUrl,
      baseHeaders: {
        Authorization: `Bearer ${idToken}`,
        'Content-Type': 'application/json',
      },
      api: async ({ path, method, headers, body }) => {
        try {
          const result = await axios.request({
            method: method as Method,
            url: `${this.baseUrl}/${path}`,
            headers,
            data: body,
          });
          return { status: result.status, body: result.data };
        } catch (e: Error | AxiosError | any) {
          if (isAxiosError(e)) {
            const error = e as AxiosError;
            const response = error.response as AxiosResponse;
            return { status: response.status, body: response.data };
          }
          throw e;
        }
      },
    });
  };
}

The body passed here is either the FormData, string , undefined or null type. We are automatically stringifying the body before passing it to the custom api client as shown here

body !== null && body !== undefined ? JSON.stringify(body) : undefined,
.

Auto-stringifying the body works fine for our tsRestFetchApi implementation but does not provide enough flexibility to developers who need access to the raw body. Ideally, when a developer chooses to create a custom client, they should be able to opt-in to auto-stringifying the body for POST/PUT requests.

We can pass in extra arguments of course, but we should have a dedicated key to guide developers and also allow developers to disable that stringify operation (if needed).

getRouteResponses accidentally deprecated

as mentioned in #189 , getRouteResponses was accidentally removed without warning.

We need to restore this method and deprecate it officially while also providing new/similar functionality to existing users.

Expects QueryClientProvider to be used when using react-query

React 17.0.1
@ts-rest/[email protected]
@ts-rest/[email protected]
@tanstack/[email protected]

My code follows closely the TS-Rest React Query example provided in the docs:

import { initQueryClient } from "@ts-rest/react-query";
import { apiContract } from "arachne/src/middleware/ts-rest-contract";

const client = initQueryClient(apiContract, {
  baseUrl: "http://localhost:4000",
  baseHeaders: {},
});

export function App(): JSX.Element {
  const { data, isLoading } = client.getFoo.useQuery([]);

  console.log("data = ", data);

  if (isLoading) {
    return <div>Loading...</div>;
  }

  return <div>Result: {data}</div>;
}

However, I get this error in the browser consoloe:

QueryClientProvider.tsx:48 Uncaught Error: No QueryClient set, use QueryClientProvider to set one
    at useQueryClient (QueryClientProvider.tsx:48:1)
    at useBaseQuery (useBaseQuery.ts:33:1)
    at useQuery (useQuery.ts:139:1)
    at Proxy.<anonymous> (index.js:15:1)
    at App (index.tsx:10:1)
    at renderWithHooks (react-dom.development.js:14985:1)
    at mountIndeterminateComponent (react-dom.development.js:17811:1)
    at beginWork (react-dom.development.js:19049:1)
    at HTMLUnknownElement.callCallback (react-dom.development.js:3945:1)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:3994:1)

I've added TanStack's React Query to my package.json because @ts-rest/react-query seems to expect it as a peer dependencies (though I couldn't see that mentioned in TS-Rest docs).

Can someone please help? I shouldn't need to use QueryClientProvider directly from TanStack's react-query if I'm using TS-Rest, right?

Hello There

Hello, we seem to have had almost the same idea.
Here is my project called Zodios that i created three month ago.
I just focused on the frontend part for now, but your separate contract is giving me ideas on the next move for me to add backend shared definition..
So maybe you can also steal some ideas from zodios.
Implementation wise, we have internally completely different beasts. I wanted aliasing to be an option to allow those who prefer to see the endpoint in their code. This turned my implementation to need a lot more typescript magic than what you did.
I defined the api definition as plain objects, i though it would be easier to read and allow for autocompletion when creating definitions. I also tied my impl more deeply into zod to allow parameters and response transformation in the definition.
I also added a complete plugin system.
So all this might inspire your impl.

i like what you have done so far. keep the good work.

Parse response on the client

As far as I've looked in the code so far, I believe zod parsing of the response on the client is not implemented yet. Is there a reason for that?

Support useQueries in @ts-rest/react-query

I just had a chat with a colleague who needed to use useQueries with react query.

I realised we don't support this! We support useQuery and useInfiniteQuery

Maybe we could use a proposed API like this? It closely reflects the RQ api.
image
image

node16 (ESM) module resolution doesn't work

Hi there,

Using @ts-rest in an ESM project that uses moduleResolution "node16" breaks typesafety:

CleanShot 2023-03-16 at 14 25 12@2x

Additional information

The project set up is overly complicated, making it harder to contribute.

  • @ts-rest is "type: module" but the tsconfig in core uses commonjs for module resolution
  • the exports field references a different declaration file ./src/index.d.ts instead of the declaration file (which is not created, why not?) of ./index.js
  • npm does not work, pnpm is required
  • pnpm i takes several minutes
  • the nx setup seems overkill for a simple library

Hono integration

Hello to all contributors!

I'm thinking about using ts-rest with hono, a "small, simple, and ultrafast web framework for the Edge".

While searching a bit I came across ts-rest-hono that two people created 6 days ago.
I haven't been able to try it yet but was wondering if:

  1. you know about it
  2. it would be possible to include it in the official integrations (maybe by inviting its creators to contribute directly here)

Maybe their plan is to reach you once it's finished (if not), maybe not, I just wanted to put the subject on the table :)

@ApiDecorator() types properties as optional when zod schema defines them as required

Hi πŸ‘‹ really interesting lib and love the integration with nestjs <3

I'm having issues with the @ApiDecorator() typing my zod schema as optional for all the properties. Maybe I'm not understanding correctly, but I assumed that zod was being used to validate those fields and therefore they would always be available to the controller (unless there was a validation error).

My schema looks like this:

image

And my nest controller route looks like this:

image

These values being optional obviously break my typing when passing down to the service etc.

Is this expected, or am I doing something wrong? Cheers again πŸ™‡

Does class-validator supported ?

Does this library have support for using class-validator for validation instead of zod? I will be using NestJS and it have very solid integration using class-validator so I wonder if class-validator is supported. Thanks

ts-rest.com: fonts are broken in a couple of places

Screenshot 2022-10-16 at 09 59 56

serif -> font-family="Virgil, Segoe UI Emoji"

This one should be easy, I guess the font just isn't linked properly / needs a better fallback

Screenshot 2022-10-16 at 09 59 51

monospace -> style="font-family:'HackNerdFontComplete-Bold', 'Hack Nerd Font', monospace

This one is harder, embedded svg so the font should probs just be a path. not even God knows how to make svgs

Middleware/Plugins

Middleware would be a great addition to @ts-rest.

I'm not sure how this should look yet in terms of the API, any feedback from usage would me massively appreciated :)

❓ 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);

Handling Router Middleware

Previously with tRPC, we were doing the following to handle an is-authenticated check for every router request:

const isAuthenticated = t.middleware(async ({ ctx, next }) => {
  if (!ctx.req.isAuthenticated()) {
    throw new TRPCError({
      code: "UNAUTHORIZED",
      message: "You are not logged in",
    });
  }
  return next({ ctx });
});

const authenticatedProcedure = t.procedure.use(isAuthenticated);

Is there a way in TS-Rest to add a function that is executed before every router handler? I can add an Express middleware, but I thought I'd ask as it'd be nice to avail of TS-Rest's common response type, data.isLoading, etc. From looking at the documentation I see I could add a 401 option to every route, e.g.

export const apiContract = c.router({
  getFoo: {
    method: "GET",
    path: "/ts/foo",
    responses: {
      200: c.response<FooResponseType>(),
      401: c.response<NoAuthResponseType>(),
    },
    summary: "Get foo",
  },
});

What is the TS-Rest recommended approach? And if it doesn't exist, is there any plan to add support for this?

For what it's worth, I'm currently handling it this way:

export const configureTSRest = (app: Express): Express => {
  app.use("/ts/*", (req, res, next) => {
    if (!req.isAuthenticated()) {
      return res.status(401).send();
    }

    return next();
  });

  createExpressEndpoints(apiContract, completedRouter, app);

  return app;
};

StrictNullChecks is required to improve performance on large projects.

I've noticed that the tsconfig.json compiler property strictNullChecks need to be enabled to get proper IntelliSense performance on useQuery on a large project. The large project is an NX mono repo, for instance. This also happens for me with other hooks like useMutation when strictNullChecks is false.

Here's a TS compilation stack trace for the said project, with strictNullChecks: false:
Screenshot_2023-01-25_at_9 20 13_AM

Here's a TS compilation stack trace for the said project, with strictNullChecks: true:
Screenshot_2023-01-25_at_9 25 49_AM

As you can see, the call stack is much larger and the compilation time shortens by at least 2 seconds.

Other users have also reported similar issues on discord. Upon investigation, it seems that strictNullChecks is required as a part of zod: colinhacks/zod#1750

Until we dig deeper to see if we can resolve this on our end, I recommend updating the docs to reflect this.

@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
}

enhance Custom Client example

Our current example code does not handle errors correctly:

export class SampleAPI {
  token: string
  constructor(params: { token: string }) {
    this.token = params.token
    this.baseUrl = 'http://localhost:3333/api'
  }
  client = () => {
    return initClient(userContract, {
      baseUrl: this.baseUrl,
      baseHeaders: {
        Authorization: `Bearer ${idToken}`,
        'Content-Type': 'application/json'
      },
      api: async ({ path, method, headers, body }) => {
        const result = await axios.request({
          method: method as Method,
          url: `${this.baseUrl}/${path}`,
          headers,
          data: body,
        }) // <--- we should catch the axios error and pass it upstream for the user to handle
        return { status: result.status, body: result.data }
      },
    })
  }
}

React Query somehow recognizes an error happens (probably because the promise does not resolve). Still, when the error's data isn't passed, we cannot access any data (or typesafe data).

We should signal users to catch and return data appropriately.

Importing @ts-rest/express in a monorepo causes module not found error

Using @ts-rest/express 3.12.1.
Node 14.19.3
React 17.0.1
Using Craco 7.0.0 to override CRA Webpack configs

I have a Lerna monorepo. I have a React (CRA) app in one workspace called Navigator, which imports a TS file from another workspace (Arachne). That TS file in Arachne imports @ts-rest/express. However, when I load the React app, I get the following error:

Failed to compile.

Module not found: Error: Package path . is not exported from package /Users/seanmcgowan/Dev/CancerIQ/umbrella2/node_modules/@ts-rest/express (see exports field in /Users/seanmcgowan/Dev/CancerIQ/umbrella2/node_modules/@ts-rest/express/package.json)
ERROR in ../arachne/src/middleware/ts-rest.ts 6:0-46
Module not found: Error: Package path . is not exported from package /Users/seanmcgowan/Dev/CancerIQ/umbrella2/node_modules/@ts-rest/express (see exports field in /Users/seanmcgowan/Dev/CancerIQ/umbrella2/node_modules/@ts-rest/express/package.json)

I've tried updating my Webpack configuration to accommodate the module mapping, e.g.

  resolve: {
    alias: {
      "@ts-rest/express": path.resolve(
        __dirname,
        "node_modules/@ts-rest/express/src"
      ),
    },
  },

But it hasn't worked. Is there a problem with @ts-rest/express not supporting Monorepos, or am I doing something wrong? Any help would be greatly appreciated.

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?

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!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.