-
Notifications
You must be signed in to change notification settings - Fork 0
Conversation
"vitest": "^2.0.1" | ||
}, | ||
"keywords": ["frontend", "web", "browser", "http", "client", "zod", "validation", "typesafe"] | ||
"name": "@lokalise/frontend-http-client", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
had to do reformatting, as library repos do not use package-lock file, so CI picked up the latest version of biome and rules file, and CI broke
I really like the fact this reduces passing as many types and tightly couples the types to the definition object, but I'm interested in the API choice of const { send: updateUser } = buildRouteDefinition({
method: 'post',
isEmptyResponseExpected: false,
isNonJSONResponseExpected: false,
responseBodySchema,
requestPathParamsSchema: pathSchema,
requestBodySchema: requestBodySchema,
pathResolver: (pathParams) => `/users/${pathParams.userId}`,
}) ? Or are you explicitly trying to avoid something like this? |
@leonaves Do you have any ideas for a better name? Not including a send function was a deliberate design choice, I wanted to decouple the contract from the implementation, backend and frontend could potentially rely on the same contract, but choose to consume them via different clients. (contract building part probably will move to shared-ts-libs eventually for the ease of sharing between BE and FE, this is primarily a PoC to illustrate the approach) |
Sorry for the slow reply, I've been mulling over this off and on for a little bit—I'm not 100% sold on the idea of the contract needing to be separate. In my example above, I probably should have been more explicit but I was implying that both would be returned: const { send: updateUser, routeDefinition } = buildRouteDefinition({
method: 'post',
isEmptyResponseExpected: false,
isNonJSONResponseExpected: false,
responseBodySchema,
requestPathParamsSchema: pathSchema,
requestBodySchema: requestBodySchema,
pathResolver: (pathParams) => `/users/${pathParams.userId}`,
}) I think it's useful the contract is exposed, but I'm not sure I see the use-case for wrapping that in a different HTTP client library, as it would need to be something that understands the I might be missing a use-case that you have already thought of, so I'm sorry if I'm making you over-explain here. I feel like the most common use-case (especially on the front-end) however is going to be importing the route definitions and then importing the |
@kibertoad I would rather go in a direction of having npm library for our REST API (e.g. For inspiration |
@leonaves @ondrejsevcik Main reason why I'm hesitant to inverse the structure and have BE own the actual client used to send requests is losing the important part of the control on the FE side. Let's consider few examples of what we can do due to the fact that the actual client is a wretch instance, owned by FE itself:
All of these things are no longer directly possible if we just create a unified client SDK. Since SDK cannot and shouldn't know how you are handling your errors and auth, you will have to fallback to manual manipulation of parameters for every request, so you'd need to put in the correct Authorization header directly into your call's headers, and do manual If we find that to be an acceptable tradeoff, and we are fine with the extra boilerplate, then @szymonchudy We could also use your perspective on this. |
Thanks for providing this proposal. Here's my take: Centralizing route definitions and exposing them as ready-to-use components seems like a great approach to improving the separation of concerns, reducing errors, enhancing maintainability, and boosting usability. I’m also fully on board with keeping the wretch API on the frontend rather than adopting an SDK-like approach. Maintaining that flexibility feels important, especially for SSR scenarios where we still “don’t know what we don’t know”. Having control over the client, for example to tailor error handling, makes sense to me. That said, I'm not entirely convinced we need this at our current scale. Our existing abstractions already do the heavy lifting for us, and while centralizing API definitions would improve things, the added complexity may not be worth it yet. Thanks for pinging me @kibertoad. I missed that one. If we decide to implement this, I'm also happy to play with the types, as I experimented with generics recently. |
That's actually a really good point - we also don't know how we are going to manage state. E. g. one way to use
Can you clarify this comment? If we are talking about exposing route definitions from the BE side, wouldn't it decrease the complexity rather than increase it, because there will be less things to know about and track on FE, and it would be making all the internal calls simply by making a 100% type-safe TS call? (as opposed to manually providing path and resolving path params, as it is now) |
Our current solution works well and follows a typical pattern – the FE provides the path, method, schemas, etc. This is the workflow everyone is familiar with. Moving this responsibility to the backend is a slight paradigm shift that always needs time to settle. Also, during the migration period, we'd need to maintain both approaches. While I see the benefits, I'm wondering if the impact justifies the extra work needed for migration and maintenance, considering our current (still small) scale. This isn't a "no" – just thinking out loud. 😊 By the way, this raises a question: Are we planning to eventually replace the current approach with this new one, or will we maintain both? |
If we decide to adopt a new approach (either this PR or the https://github.com/lokalise/polyglot-service/pull/1188), I'd say that the goal will be to eventually migrate everything over to it. It doesn't make sense to maintain two different ways to do the same thing, when we consider one of them to be better.
That I don't have a strong opinion on. My only point is that if we want to do the change, now is a good time - before we started implementing all the new frontends. |
As a FE developer I only care about the data that I need and I care not about how that data is requested. Essentially I need a function to call, need to know the params and need to know the type of the data that will be returned. I am all for this change. |
I contacted everyone involved, and here’s the summary: There are three options on the table:
Thought process
RecommendationTo wrap it up, I recommend implementing route definitions as proposed by Igor. This is a relatively low-risk endeavor that could serve as a stepping stone toward more sophisticated solutions. In parallel, I suggest implementing Mateusz's proof of concept in Polyglot with a defined timeline (e.g., 3-6 months). After this period, we can evaluate its impact and either revert Polyglot to the route definition solution or move forward with broader |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you considered the wretch addon mechanism for the API definition?
And example of this might be:
export const SomePropsSchema = z.string()
export type SomeProps = z.infer<typeof SomePropsSchema>
export interface MyApi {
doSomething: <T extends MyApi, C, R>(this: T & Wretch<T, C, R>, prop: SomeProps) => this
}
export const myApi: () => WretchAddon<MyApi> = () => {
return {
wretch: {
doSomething(prop) {
return this.url('/some/path') // Build the request
},
},
}
}
And then a client of the API would look something like this
const client = wretchClient.addon(myApi)
const value = await client.doSomething('The string').get();
… into feat/route-definitions # Conflicts: # package.json
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, can't wait to see it flying 🚀
Changes
This is a proposal for a new API for defining REST API requests.
Key idea behind it: backend owns entire definition for the route, including its path, HTTP method used and response structure expectations, and exposes it as a part of its API schemas. Then frontend consumes that definition instead of forming full request configuration manually on the client side.
This reduces amount of assumptions FE needs to make about the behaviour of BE, reduces amount of code that needs to be written on FE, and makes the code more type-safe (as path parameter setting is handled by logic exposed by BE, in a type-safe way).
Checklist
major
,minor
,patch
orskip-release