-
-
Notifications
You must be signed in to change notification settings - Fork 105
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
Support HATEOAS #1187
Comments
Hi @SenseiMarv, let me put a vote label on this feature. As for your use case, what are you currently doing/using? I'm also curious why are you requesting this feature vs using another tool that presumably handles HATEOAS already? Thanks! |
Hey there, chipping in with an extra add-on to this request 😄 Adding on to that, supporting that those URLs are "partially" filled out, yet still a template would be amazing too. E.g. having an OpenAPI like this (sticking loosely with the bank account example): /accounts/{accountId}/transactions:
parameters:
- name: accountId
in: path
required: true
schema:
type: string
get:
parameters:
- name: limit
in: query
schema:
type: integer
- name: offset
in: query
schema:
type: integer
- name: sender
in: query
schema:
type: string
description: Filters transactions by sender including this string we could get a response from our backend linking to this endpoint that looks like this: {
"_links": {
"next": {
"href": "https://myhost/accounts/123456/transactions?limit=10&offset=30{&sender}"
"templated": true
},
...
},
...
} The idea here would then to still take the Probably would require making all of those options of the request optional though as one can never know what parameters are already pre-filled (at least not at code-generation-time). |
Thanks for the lightning fast response! We are currently trying out Hey API to see if we can use API client generation for our OpenAPI files. We use our OpenAPI files as a single source of truth and do not want to write/generate them using contracts with tools like ts-rest or Effect. Hey API is particularly interesting as it provides native TanStack Query integration and soon Zod integration. As we may build our production software on TanStack Query in the future and already use Zod heavily for form validation, this library brings a lot to the table. At the moment we write our frontend client code ourselves, but it might be interesting to move to Codegen. We are currently trying out Hey API with a small prototype and have run into this hurdle. It is a bit of a blocker for us as we do not want to parse our valid URLs to extract the path parameters. The only alternative would be to copy/edit the generated code to take our links - which would defeat the purpose of Codegen. |
@SenseiMarv this seems like a quite large feature to implement so it might take a while unless there's an enormous interest in it. If resolving #452 would unblock you, please let me know and I can prioritise that one |
@mrlubos Absolutely understandable if this takes longer to support if it is complex to implement. Luckily, the minimal workaround to enable us to still use our links is very simple :) Changing the Codegen to instead of generating this /**
* Find pet by ID
* Returns a single pet
*/
export const getPetById = <ThrowOnError extends boolean = false>(
options: Options<GetPetByIdData, ThrowOnError>,
) => {
return (options?.client ?? client).get<
ThrowOnError,
GetPetByIdResponse,
GetPetByIdError
>({
...options,
url: '/pet/{petId}',
});
}; To allow overriding the url by placing the spread options below /**
* Find pet by ID
* Returns a single pet
*/
export const getPetById = <ThrowOnError extends boolean = false>(
options: Options<GetPetByIdData, ThrowOnError>,
) => {
return (options?.client ?? client).get<
ThrowOnError,
GetPetByIdResponse,
GetPetByIdError
>({
url: '/pet/{petId}',
...options,
});
}; And then changing the type OptionsBase<ThrowOnError extends boolean> = Omit<RequestOptionsBase<ThrowOnError>, 'url'> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
* custom client.
*/
client?: Client;
}; to no longer omit type OptionsBase<ThrowOnError extends boolean> = RequestOptionsBase<ThrowOnError> & {
/**
* You can provide a client instance returned by `createClient()` instead of
* individual options. This might be also useful if you want to implement a
* custom client.
*/
client?: Client;
}; Is enough so we can define our own url in case we have a HATEOAS link: const { data: pet } = await getPetById({
url: '/pet/{petId}',
path: {
// random id 1-10
petId: Math.floor(Math.random() * (10 - 1 + 1) + 1),
},
}); (I've used the linked StackBlitz demo from the homepage for my example here) |
This all looks pretty straightforward and no objection to these changes. So no need for #452 at all? And can you just explain for me, how will you know which function to call with your custom URL? |
@mrlubos Good to hear! Upon a second look, it seems like #452 is going into a slightly different direction of wanting access to the URLs by having Hey API export them. This is, as we already clarified, more about "overriding" them. To stay with the Petstore example from the demo on the homepage: Let's imagine that the response body of {
// ...
"_links": {
"delete": {
"href": "https://petstore3.swagger.io/api/v3/pet/5"
}
}
} The link provided can now be used for the delete endpoint and will delete the pet we've just fetched: const [pet, setPet] = useState<Pet>();
const onFetchPet = async () => {
const { data: pet } = await getPetById({
path: {
// random id 1-10
petId: Math.floor(Math.random() * (10 - 1 + 1) + 1),
},
});
setPet(pet);
};
const onDeletePet = async () => {
await deletePet({
url: pet._links.delete.href,
});
setPet(undefined);
}; So, in general, you know where to use the provided links by semantics or as defined in the API docs. I hope this answers your question? Two notes about this:
type OptionalFlag = 'optional';
export type Embedded<
TItem,
Optional extends OptionalFlag | undefined = undefined,
> = Optional extends OptionalFlag
? { _embedded?: { items: TItem[] } }
: { _embedded: { items: TItem[] } };
// And then the optional flag can be used like this: Embedded<{ name: string }, 'optional'>;
|
After thinking about it, one comment about my note 2 above: |
Description
Our API uses Spring HATEOAS (short explanation: https://en.wikipedia.org/wiki/HATEOAS). So a typical API response body might look like this:
Note the added
_links
at the end of the response body. This contains several valid links (URLs) to other endpoints. These endpoints normally use Path Parameters, but are already fully resolved here.If we now want to use Hey API, we run into the problem that we already have a fully valid link, but there is no way to use it in the generated code. We would have to somehow parse our valid URL, extract the path parameters and then pass them to the generated functions with the
path
props. This is really cumbersome and involves introducing parsing that could fall apart with any change to the API.Is there any way to add support for HATEOAS so that we can use our links directly? A cheap solution would be to support passing of a URL to the generated functions, which as far as I can see has already been requested: #452.
The text was updated successfully, but these errors were encountered: