GithubHelp home page GithubHelp logo

trpc / v10-playground Goto Github PK

View Code? Open in Web Editor NEW
12.0 12.0 3.0 267 KB

tRPC v10 procedure play

Home Page: https://stackblitz.com/github/trpc/v10-playground?file=src%2Fserver%2Findex.ts,src%2Fclient.ts,src%2Fserver%2Frouters%2FpostRouter.ts&view=editor

HTML 0.40% TypeScript 99.60%

v10-playground's Introduction

tRPC

tRPC

Move fast and break nothing.
End-to-end typesafe APIs made easy.

codecov weekly downloads MIT License Discord
Twitter

Demo

The client above is not importing any code from the server, only its type declarations.


Note

You are looking at the next-branch of tRPC which is the current work in progress representing version 11.

  • The functionality is stable and can be used in production, but we may do small breaking API-changes between patches until we reach 11.0.0
  • The packages are published with the next-tag on npm
  • For the list of changes made, see https://trpc.io/docs/v11/migrate-from-v10-to-v11

Intro

tRPC allows you to easily build & consume fully typesafe APIs without schemas or code generation.

Features

  • ✅  Well-tested and production ready.
  • 🧙‍♂️  Full static typesafety & autocompletion on the client, for inputs, outputs, and errors.
  • 🐎  Snappy DX - No code generation, run-time bloat, or build pipeline.
  • 🍃  Light - tRPC has zero deps and a tiny client-side footprint.
  • 🐻  Easy to add to your existing brownfield project.
  • 🔋  Batteries included - React.js/Next.js/Express.js/Fastify adapters. (But tRPC is not tied to React, and there are many community adapters for other libraries)
  • 🥃  Subscriptions support.
  • ⚡️  Request batching - requests made at the same time can be automatically combined into one
  • 👀  Quite a few examples in the ./examples-folder

Quickstart

There are a few examples that you can use for playing out with tRPC or bootstrapping your new project. For example, if you want a Next.js app, you can use the full-stack Next.js example:

Quick start with a full-stack Next.js example:

# yarn
yarn create next-app --example https://github.com/trpc/trpc --example-path examples/next-prisma-starter trpc-prisma-starter

# npm
npx create-next-app --example https://github.com/trpc/trpc --example-path examples/next-prisma-starter trpc-prisma-starter

# pnpm
pnpm create next-app --example https://github.com/trpc/trpc --example-path examples/next-prisma-starter trpc-prisma-starter

# bun
bunx create-next-app --example https://github.com/trpc/trpc --example-path examples/next-prisma-starter trpc-prisma-starter

👉 See full documentation on tRPC.io. 👈

Star History

Star History Chart

Core Team

Do you want to contribute? First, read the Contributing Guidelines before opening an issue or PR so you understand the branching strategy and local development environment. If you need any more guidance or want to ask more questions, feel free to write to us on Discord!


Alex / KATT
👋 Hi, I'm Alex and I am the creator of tRPC, don't hesitate to contact me on Twitter or email if you are curious about tRPC in any way.

Project leads

The people who lead the API-design decisions and have the most active role in the development


Julius Marminge

Alex / KATT

Active contributors

People who actively help out improving the codebase by making PRs and reviewing code


Nick Lucas

Flo

Sachin Raja

Special shout-outs


Chris Bautista

Theo Browne
Ahmed%20Elsakaan
Ahmed Elsakaan

James Berry

Kamil Ogórek

Sponsors

If you enjoy working with tRPC and want to support us, consider giving a token appreciation by GitHub Sponsors!

🥇 Gold Sponsors

Tola
Tola

🥈 Silver Sponsors

Cal.com,%20Inc.
Cal.com, Inc.

🥉 Bronze Sponsors

Echobind
Echobind
Dr.%20B
Dr. B
Flylance
Flylance

😻 Smaller Backers

Ahoy%20Labs
Ahoy Labs
Dyaa
Dyaa
Brooke
Brooke
Max%20Greenwald
Max Greenwald
Tom%20Ballinger
Tom Ballinger
Faraz%20Patankar
Faraz Patankar
Adam%20Slaker
Adam Slaker
Dmitry%20Maykov
Dmitry Maykov
Chris%20Bradley
Chris Bradley
Ahmed%20Elsakaan
Ahmed Elsakaan
Hampus%20Kraft
Hampus Kraft
Illarion%20Koperski
Illarion Koperski
SchlagerKhan
SchlagerKhan
Jared%20Wyce
Jared Wyce
fanvue
fanvue
Andrew%20Brown
Andrew Brown
Ascent%20Factory
Ascent Factory
Unkey
Unkey
Jonas%20Strassel
Jonas Strassel
Jordy
Jordy
Daniel%20Burger
Daniel Burger
Scale%20Leap
Scale Leap
Drew%20Powers
Drew Powers
Drizzle%20Team
Drizzle Team
Liran%20Goldman
Liran Goldman
Spencer%20McKenney
Spencer McKenney
Proxidize
Proxidize
Oskar%20Hertzman
Oskar Hertzman
Imamuzzaki%20Abu%20Salam
Imamuzzaki Abu Salam

All contributors ✨

A table of avatars from the project's contributors


Powered by Vercel

v10-playground's People

Contributors

katt avatar mmkal avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

v10-playground's Issues

Awkward reference to dot-separated strings

const posts = await client.query(queries['post.all']);

In my opinion, this syntax is unnatural in JS/TS. It’s acceptable when the value is just a string, but in this case, it’s a dot-separated string which is the property of an object. It feels quirky and IMO really exposes this as a DSL, a “magic string”.

Since we’re considering an alternative syntax anyway, is there a stance against making procedures referenceable as objects? Example:

const posts = await client.query(queries.post.all);

thanks!

Chapter 1) The Router API

The router API

Challenges with the current implementation

  • TypeScript performance. Each procedure added creates a new Router that TypeScript chokes on as the underlying complexity grows non-linearly with each added procedure - each procedure basically has its "own instance" within the compiler even if it's flattened and unaffected at runtime.
  • I find that those queries are hard/unintuitive to write, I find myself counting parentheses and curly brackets all too often. Arguably, your suggestions with the resolver API could also address this.
  • No CMD+click

Suggested implementation

  • Flat router
  • No automatic prefixing (breaks jump to definition / CMD+click)
import { z } from 'zod';
import { initTRPC } from './trpc/server';

const trpc = initTRPC<Context>();

const postRouter = trpc.router({
  queries: {
    postById: null as any /* resolver implementation is a separate discussion */
    postAll: null as any /* resolver implementation is a separate discussion */
  },
  mutations: {
   postAdd: null as any /* resolver implementation is a separate discussion */
});

const userRouter = trpc.router({
  queries: {
    userAll: null as any /* resolver implementation is a separate discussion */
  },
);

const appRouter = trpc.mergeRouters(
  postRouter, 
  userRouter, 
  // ...
)

Make the RPC call as property access on the Proxy?

Since you are using JS Proxies why not go all in and implement the rpc calls directly as a property access?

Eg. instead of

client.query(queries.greeting, {hello: 'string'})

why not allow

client.greeting({hello: 'string'})

Although with this it could be trickier for the go-to-definition implementation.

TypeScript Performance testing

Goal: Make sure that we can have a big backend with this new structure and still keep good DX

How:

  • Create a router with a few hundred(?) procedures
  • Mix types of different types - some with zod inputs some without

Tests:

  • Update an input in the server
  • See how long it takes to infer changes on the client

Resolver definition API too terse?

The input and resolver are now just positional arguments

trpc.resolver(
  trpc.zod(
    z.object({
      hello: z.string()
    })
  ),
  (params) => {
    return {
      data: {
        greeting: "hello " + params.ctx.user?.id ?? params.input.hello
      }
    };
  }
);

to me this feels bit more cryptic, especially when adding context creation too, than the current version where the resolver definition is an object.
Would this be possible here as well?

trpc.resolver({
  input: z.object({
    hello: z.string()
  }),
  resolver(params) {
    return {
      data: {
        greeting: "hello " + params.ctx.user?.id ?? params.input.hello
      }
    };
  }
});

I believe the object version would be also more future proof since it is easily extendable without breaking existing apis.

API thoughts

Proxy 👍

I think using Proxy makes sense, though I don't understand all the concerns mentioned here #21. Support for it is really good now so I don't think that should be a concern. It's a pre-requisite for some of the API ideas I have below.

The router API 😐

I'm pretty ambivalent here. I like the fact that the procedure names are now object keys instead of just strings. But the new API requires an extra level of nesting, which I don't love.

trpc.router({
  queries: {
    hello: _ => ({data: "Hello"}),
    helloagain: _ => ({data: "Hello again"}),
  }
})

trpc.router()
  .query("hello", _ => ({data: "Hello"})
  .query("hello", _ => ({data: "Hello"})

More on this later.

The resolver API 😕

I don't think people like purely functional APIs. The success of Zod is a testament to this. The io-ts API is purely functional and it's really annoying to make things optional, add default values, etc. Wrapping a schema in t.optional(...) is just more difficult than appending .optional() to it.

All that to say, the trpc.resolver(trpc.zod(...)) thing just rubs me the wrong way.

trpc.router({
  queries: {
    hello: trpc.resolver(
      trpc.zod(
        z.object({
          hello: z.string(),
        }),
      ),
      (params) => {...}
    );
  }
})

If trpc.resolver was variadic and could accept an arbitrary number of potentially context-modifying middlewares, that would almost be cool enough for this API to be worth it (almost). But I'm pretty sure that's impossible. Sure, you can increase the number of inputs it can accept by just adding more and more overloads, but that's too hacky even for me.

But even if the variadic thing was possible, I find this quite a bit less pleasant to read and write than a chained version.

That said, I see what you're trying to do here. I like the idea of the input validation being "just another middleware" and I understand the limitations with the object-based API. Here's an alternative API that I quite like:

trpc.router({
  queries: {
    hello: trpc.resolver()
      .input(z.object({ test: z.string() }))
      .modifyContext(params => ({ ctx: ... })) 
      .resolve(params => ...)
  }
})

Basically you call trpc.resolver for each endpoint, and it provides you with a bunch of convenience methods for "appending" certain types of middlewares to the chain. It's easy to add additional methods (middleware types) to the Resolver object. You get auto-completion on the method names, so it's easy for users to discover all the different kinds of middleware logic they can implement. I personally really like the "pipeline" feel of it.

Optionally trpc.resolver() could accept a v9-style object-based resolver definition for users who prefer that style.

trpc.router({
  queries: {
    hello: trpc.resolver({
      input: z.object({ ... }),
      resolve(params){
        return { ... } 
      }
    })
  }
})

Though there's an API that's even better. It avoids a bunch of repetitive calls to trpc.resolver() and doesn't represent such a dramatic shift from the v9 API.

trpc.router()
  .query("hello", route => route
    .input(z.object({ test: z.string() }))
    .modifyContext(params => ({ ctx: ... })) 
    .resolve(params => ...))
  .query("helloAgain", route => route
    .input(z.object({ test: z.string() }))
    .modifyContext(params => ({ ...params.context, newKey: "hey yo" })) 
    .resolve(params => ...))

Basically this closure API is a "power user feature" for users who prefer to write pipeline-style resolvers. It's easy to continue supporting the v9 API too. I think this is pretty close to optimal.

The client API 🤔

I'm on record as disliking the current tuple-based API.

I think this proposed API is better but still doesn't feel RPC-like to me.

const greeting = await client.query(queries['post.add'], { hello: 'world' });

I think making the API worse in order to support Cmd-Click/"Go to definition" is a really bad tradeoff. If you put this to a vote on Twitter I'm guessing a vast majority of users would prefer a cleaner-looking client API to having CMD+click support. (I'm willing to be proven wrong here, but I'm very confident in this.)

As for my ideal client API, I think it should be this:

const greeting = await trpc.query.post.add({ hello: 'world' });

This is an RPC library. Any API that doesn't feel like you're "calling" the remote function is bad, bordering on unacceptable. This is roughly how the API looked in v0.x and I would be super freakin happy to see it come back. If you don't do this, I suspect an alternative to tRPC will come along with this API and end up winning.

I submitted a POC of this API here (types only, no runtime implementation): #22

Chapter 4) The React `useQuery()` / `useMutation`

The React API

Suggestion A) Similar to existing API

const trpc = createReactQueryHooks<AppRouter>()

const {queries, mutations, useQuery, useMutation} = trpc;

/// usage

function MyComponent() {
  // you can CMD+Click `postById` and jump straight to the backend resolver
  const query1 = useQuery([queries.postById, { id: 1 }], { /* react-query opts  */ })

  // Also same query and will be usable, but you lose jump to definition
  const query2 = useQuery(['postById', { id: 1 }], { /* react-query opts  */ })


  const mutation1 = useMutation(mutations.postUpdate); // <-- jump to definition by clicking `postUpdate`
  const mutation2 = useMutation('updatePost');

  // later used as `mutation.mutate(input)` or `mutation.mutateAsync(input)`

}

Suggestion B) Pseudo-call within hook

trpc.useQuery(client.query.postById({ id: 1 }), { /* react-query options */ })

Suggestion C) Skipping the tuple

Related decision

  • Revive trpc/trpc#1058
  • Add new input argument onto the react-query options
    • If optional, keep optional, if required, require to pass opts
const trpc = createReactQueryHooks<AppRouter>()

const {queries, mutations, useQuery, useMutation} = trpc;

/// usage

function MyComponent() {
  // you can CMD+Click `postById` and jump straight to the backend resolver
  const query1 = useQuery(queries.postById, { 
    input: { id: 1 }, 
    /* [...] other react-query opts */
  }) 

  // Also same query and will be usable, but you lose jump to definition
  const query2 = useQuery('postById', {
    input: { id: 1 },
    /* [...] other react-query opts */
  }) 
}

Consider this

Tried to write a reply to this on my phone but too hard! Yes this is pretty much what I was thinking. A couple of ways it could be simplified even further occur to me tho:

  1. No linear generics at all. This might prove impossible if the compiler loses track of property types somewhere along the way, but here's a simplified example:
interface Params {
  input: unknown
  output: unknown
  context: unknown
}

const getClientThatUsesSpecificParams = <P extends Params>(params: P) => {
  return {
    useInput: (input: P['input']) => 123,
    getOutput: () => params.output,
  }
}

const specific = getClientThatUsesSpecificParams({
  input: {foo: 'abc'},
  output: [123, 456],
  context: {bar: 987},
})

TypeScript doesn't need specific typeargs for TInput, TOutput, TContext to keep track of them:

image

  1. Maybe there could be an IO type to generalise the _in and _out suffixes?
interface IO<I, O> {
  in: I
  out: O
}

interface Params< 
  TContextIn = unknown,
  TContextOut = unknown,
  TInputIn = unknown,
  TInputOut = unknown,
  TOutputIn = unknown,
  TOutputOut = unknown,
> {
  ctx: IO<TContextIn, TContextOut>
  input: IO<TInputIn, TInputOut>
  output: IO<TOutputIn, TOutputOut>
}
  1. Both together!?!!?
interface IO {
  input: unknown
  output: unknown
}

interface Params {
  ctx: IO
  input: IO
  output: IO
}

Originally posted by @mmkal in #33 (comment)

tRPC name change?

Update: The name I'm proposing here is zapi (Zero-API). I have @zapi/*-namespace on npm + zapi-package + @zapijs/* on github and own the name zapijs.com


I have been pondering this for a long over the name... I've heard people say or try to spell "trcp"/"tprc"/etc.. also heard people mixing it up with gRPC, which is a quite different project..

Many people don't really know what RPC is.. it's kinda old tech that is getting a revival. I personally like the name tRPC & maybe this is an adoption curve thing, but I think the name of RPC is too technical and I feel how I've worded everything around tRPC I think it's a bit of a walled garden to adopt it, beginners in JS-land might be scared to pick up this tool, even if it's the best tool they could possibly use.

The thing is, tRPC is one of the fastest ways there is to build & use APIs in general. With one tool you can have most of the tools you need to quickly and safely build out a backend and the tools to build the client in a rapid way. Very few other Node-based server tools provide runtime input validation as a first-class citizen (which I've never understood why).

Long-term, I don't care if tRPC stays true to "RPC", although I think doing is the best for the DX. But I will focus on what is best for DX - with DX being is a wider concept than what you see today:

  • .. encompass building OpenAPI-compliant APIs with tRPC as a backbone.
  • .. automatically generate SDKs for other languages
  • .. solutions for how you easily [edge] cache procedure's data
  • .. might be able to chain procedures in one request in order to avoid query waterfalls
  • [..]

Short-term, when releasing this initial version, I want to have a smooth migration path. With this suggestion, it'll be easy to install the new package(s) in parallel and do a migration incrementally.

I have a name in mind already, with a [name]js.com & @[name]/*-org on npm already. If y'all like that is a separate discussion, but want to bring up the idea of even changing the name first.

Naming functions that return middlewares (and beyond)

Naming of functions that returns functions and naming of functions that returns functions that return middlewares 🤦

What to name the createUseNewContext middleware?

Current usage:

const useNewContextFactory = createUseNewContext<TestContext>();
const useIsAuthed = useNewContextFactory((params) => {
  if (!params.ctx.user) {
    return {
      error: {
        code: 'UNAUTHORIZED',
      },
    };
  }
  return {
    ctx: {
      ...params.ctx,
      user: params.ctx.user,
    },
  };
});

// in resolver
const procedure = resolver(
  useIsAuthed(),
  () => {
    // ..
  }
)

Do we like the useX format? useX format needs changing

useZod() returns a middleware for zod.

  • Might Definitely collides with React
  • I was more thinking in the lines of Express' app.use() when I came up with it.
  • Alternatives?

Chapter 3) The raw client API

The raw client API

The API you'd use if you are writing like a CLI or something with tRPC.

Client API Proposal with Proxy:

  • Proxy to the rescue!
  • Jump from the client to the and jump straight into the backend definition with 1 click!
  • Implemented in #21
import type { appRouter } from './server';
import { createClient } from '@trpc/client';

const client = createClient<typeof appRouter>();

async function main() {
  // you can CMD+click `postById` here and jump straight into your backend
  const byId1 = await client.queries.postById({ input: { id: '1' }});

  // with meta data:
  const byId2 = await client.queries.postById({ 
    input: { id: '2' },
    context: {
      batch: false,
    }
  });


  // For backwards compatability:
  const list = await client.query('postList');
}

The reasoning with input: {} as an options object is that sometimes we might want to pass additional configuration and when input is optional it's a bit iffy to do client.query(undefined, {context: {foo: 'bar'}}

Chapter 2) The Resolver API + middlewares

The Resolver API and middlewares

Problems with the current implementation

  • Middlewares are hard to reuse
  • I find them quite fiddly to write - counting brackets etc.

Suggestion A) Pipes

Standalone route

// single resolver
const myProcedure = trpc.resolver(
  trpc.zod( z.object({ id: z.string }) ),
  ({ ctx, input }) => {
    return 'my data goes here'
  }
)```

### In a router
```ts
export const appRouter = trpc.router({
  queries: {
    // procedure with input validation called `greeting`
    greeting: trpc.resolver(
      trpc.zod(
        z.object({
          hello: z.string(),
        }),
      ),
      (params) => {
        return {
          data: {
            greeting: 'hello ' + params.ctx.user?.id ?? params.input.hello,
          },
        };
      },
    ),
  }
)

Inferring errors

const isAuthed = trpc.newContext((params) => {
  if (!params.ctx.user) {
    return {
      error: {
        code: 'UNAUTHORIZED',
      },
    };
  }
  return {
    ctx: {
      ...params.ctx,
      user: params.ctx.user,
    },
  };
});

Reusable middleware

// with re-usable middleware
const isAuthed = trpc.newContext((params) => {
  if (!params.ctx.user) {
    return {
      error: {
        code: 'UNAUTHORIZED',
      },
    };
  }
  return {
    ctx: {
      ...params.ctx,
      user: params.ctx.user,
    },
  };
});

export const appRouter = trpc.router({
  queries: {
    // procedure with input validation called `greeting`
    postCreate: trpc.resolver(
       isAuthed,
      (params) => {
        return {
           // ...
        };
      },
    ),
  }
)

Suggestion B) Closures & Chains

Standalone route

// single resolver
const myProcedure = trpc.route(route => route
  .input(z.object({hello: z.string}))
  .resolve(req => {
    return req.input.hello;
  })
);

In a router

export const appRouter = trpc.router({
  queries: {
    // procedure with input validation called `greeting`
    greeting: route => route
      .input(
        z.object({
          hello: z.string(),
        })
      )
      .resolve(req => {
        return {
          data: {
            greeting: 'hello ' + params.ctx.user?.id ?? params.input.hello,
          },
        };
      }),
  },
});

Reusable middleware

// with re-usable middleware
const isAuthed = trpc.middleware(params => {
  if (!params.ctx.user) {
    return trpc.error({
        code: 'UNAUTHORIZED',
    });
  }
  return {
    ctx: {
      ...params.ctx,
      user: params.ctx.user,
    },
  };
});


export const appRouter = trpc.router({
  queries: {
    // procedure with input validation called `greeting`
    postCreate: route => route
      .use(isAuthed)
      .resolve(params => {
        return {
          // ...
        };
      }),
  },
});

Inferring the right error on `contextSwapperMiddleware`

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.