Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose TRPC transformer from api package #1189

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

ochicf
Copy link

@ochicf ochicf commented Sep 21, 2024

Context

In #1110, a problem with superjson caused the necessity of swapping transformer to devalue. Code for superjson is spread in 4 different places:

  • packages/api/src/trpc.ts
  • apps/nextjs/src/trpc/query-client.ts
  • apps/nextjs/src/trpc/react.tsx
  • apss/expo/src/utils/api.tsx

Currently all of those places directly import superjson, which for this specific transformer is not super problematic (as its interface matches 1 to 1 to the expected transformer in tRPC), but it makes it inconvenient to swap to another or do any customization to its behavior, as it needs to be changed in all places.

Proposal

Define the transformer in the api package and export it so it can be used in all places. Besides the benefit of applying DRY, it moves the entire responsability of the transformer where it belongs (close to the API) and the apps are just consumers.

Clarifications

Swapping transformers

Now swapping transformers will be super easy and convenient and fully transparent to developers in their local machines, though underneath it's a big breaking change as it drastically changes how the data is being sent through the API. This is why I found it necessary to add a disclaimer in that file just in case.

For example, if there are already distributed apps with the superjson transformer and there's a swap to devalue, all of those app's API calls will break when the API using devalue gets released.

Of course, this would happen before as well and it's out of the scope of this repo, but IMO as now it's easier and "seems prepared to do it" adding the disclaimer made sense.

Expo app

Including the module

To include the @acme/api/module I had to add it in the tsconfig.json's compilerOptions.paths section, pointing directly to the file.

I'm not really sure if this is correct, but it's how I managed to make it work.

Restricting usage from @acme/api package

Since the @acme/api package is now a direct dependency in the expo app, I've added some eslint rules to prevent importing values directly from it, or from modules other than /transformer.

Example of imports

/*6*/  import type { AppRouter } from "@acme/api"; // <- correct: imports type
/*7*/  import type { hello as _1 } from "@acme/api/hello"; // <- correct: imports type
/*8*/  import { createCaller as _2 } from "@acme/api"; // <- incorrect: imports value
/*9*/  import { transformer } from "@acme/api/transformer"; // <- correct: imports value from allowed module
/*10*/ import { world as _3 } from "@acme/api/world"; // <- incorrect: imports value from restricted module
[...]/create-t3-turbo/apps/expo/src/utils/api.tsx
   8:1  error  '@acme/api' import is restricted from being used. Only type imports from '@acme/api' are allowed                            @typescript-eslint/no-restricted-imports
  10:1  error  '@acme/api/world' import is restricted from being used by a pattern. Only certain modules from '@acme/api' can be imported  @typescript-eslint/no-restricted-imports

✖ 2 problems (2 errors, 0 warnings)

Requirements

renovate bot and others added 8 commits September 19, 2024 20:21
… than `@acme/api/transformer`

Example of imports
```ts
import type { AppRouter } from "@acme/api"; // <- correct: imports type
import type { hello as _1 } from "@acme/api/hello"; // <- correct: imports type
import { createCaller as _2 } from "@acme/api"; // <- incorrec: imports value
import { transformer } from "@acme/api/transformer"; // <- correct: imports value from allowed module
import { world as _3 } from "@acme/api/world"; // <- incorrect: imports value from restricted module
```
Results of running `pnpm lint`:
```sh
[...]/create-t3-turbo/apps/expo/src/utils/api.tsx
   8:1  error  '@acme/api' import is restricted from being used. Only type imports from '@acme/api' are allowed                            @typescript-eslint/no-restricted-imports
  10:1  error  '@acme/api/world' import is restricted from being used by a pattern. Only certain modules from '@acme/api' can be imported  @typescript-eslint/no-restricted-imports

✖ 2 problems (2 errors, 0 warnings)
```
@@ -15,9 +15,10 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@acme/api": "workspace:*",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have Expo shipped tree shaking yet? If not I think including this as a prod dep will leak backend code

Copy link
Member

@juliusmarminge juliusmarminge Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

@ochicf ochicf Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, didn't though of that. I'm not sure how to check if it's happening (I'm quite new with Expo and RN). Also, not sure if updating SDK is easy or not and should be done in this PR or in a different one.

Another option would be to move the transformer logic into a separate package so we are 100% sure it doesn't bring any BE code. For example @acme/transformer or @acme/api-transformer (though IMHO it feels much better to have it as a module in @acme/api).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am personally scared of adding an api package as a dependency, regardles of wether or not Expo does tree-shaking correctly or not. Might not be an issue, but it's something to be mindful of.

I personally have a @acme/shared package that exports common utils/stuff that is safe to be shared everywhere. Adding another package just for the tranformer export might be a bit verbose for this repo though

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, yeah it makes sense. I'll prepare that and revert the api package to just dev dependency in Expo.

Comment on lines 4 to 12
/**
* tRPC transformer, used to serialize/deserialize data between client and server.
* This export is used internally in `@acme/api` and in both `apps/nextjs` and `apps/expo`.
*
* ! IMPORTANT: changing this is a BREAKING CHANGE !
* ? Even though this helps swapping transformers, bear in mind that distributed Expo apps
* ? will have the previous transformer bundled in their device, which will cause a mismatch
* ? in how the data is sent and received (basically, all API requests will fail).
*/
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good callout. add a @see comment to trpc docs here too as a reference. not sure many people have used the "full input/output syntax" before

@dBianchii
Copy link
Contributor

Ok, so swapping the transform is for cases where the current superJSON might not be enough? When will that be the case?
Isn't the date issue for mutations a problem with superJSON directly?

@ochicf
Copy link
Author

ochicf commented Oct 7, 2024

@dBianchii

Ok, so swapping the transform is for cases where the current superJSON might not be enough? When will that be the case?

Yes. Though bear in mind that this refactor ends up with having the transformer in a single place, which would allow you to easily/consistently enhance superjson (for example to perform serialisation for custom entities, see recipes section in superjson documentation).

Isn't the date issue for mutations a problem with superJSON directly?

In this case yes. For this specific issue, having the transformer logic in a single place would allow to to do a local fix on top of superjson's deserialise function, at least as a temporary fix while the fix is done at their end (this was the first approach I did in my project before swapping to devalue).


I think having transformer in a single place is a good thing also to let developers choose the transformer they prefer, not due to any bug (for example, they may like more devalue due to performance/footprint)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants