T
TanStack2w ago
conscious-sapphire

recommended pattern to work with

hi, i love this library but there is one catch with it and is when adding a new service/endpoint requests, it feels kinda a slow process (at least the way im doing it) which is the following add a new directory in the api folder with the endpoint name make an index.ts file with the querykeys for that service make a get.ts, post.ts, patch.ts, delete.ts for the main requests (this is mainly because i need to have different types for the responses, even if sometimes is the same entity, because some of them have different fields yada yada putting the queries in a context provider file so i can have access to them but all of these feel numbing and dumb and too complex for the sake of it, but i cant really come up with something that is quite right, i think i can omit the step of using the context but idk hjadsfjhdsaj i think i might ended up making a whole mess for nothing if someone wanna share a project like a midsize or whatever i would be really really grateful
5 Replies
foreign-sapphire
foreign-sapphire2w ago
What do you need the context provider for?
quickest-silver
quickest-silver2w ago
Apologies, I cannot share my work project, but I can share the folder structure we use
src/api/
index.ts
$service/
querykeys.ts
queries/
queryA.ts
queryB.ts
mutations/
mutationA.ts
mutationB.ts
src/api/
index.ts
$service/
querykeys.ts
queries/
queryA.ts
queryB.ts
mutations/
mutationA.ts
mutationB.ts
Each of these files is purely in the format
import API from ...

export const createQueryOptions = (args: Args) => {
return queryOptions({
queryKey: queryKeys.queryA(args),
queryFn: API.service.get(args)
})
}
import API from ...

export const createQueryOptions = (args: Args) => {
return queryOptions({
queryKey: queryKeys.queryA(args),
queryFn: API.service.get(args)
})
}
And finally, in the src/spi/index.ts file we have an object with all options that matches the folder structure, just for the ability to import one object and get intellisense for all endpoints in the application:
export const NXQuery = {
service: {
queryA,
queryB,
mutationA,
mutationB,
}
}
export const NXQuery = {
service: {
queryA,
queryB,
mutationA,
mutationB,
}
}
To make a query this means 1. create endpoint in API (normally just a fetch request with types/zod schema 2. create file with query options 3. update queryKeys object with query 3. attach query to shared query object I somewhat agree that this can be tedious, but once you have your main endpoints made, the rate at which you are making new ones is pretty low, and this is therefore not a huge pain point (at least at my company). With this said, I have been working on a vite plugin which would autogen a lot of this as soon as you make a .ts file in the above folder structure, but otherwise we have been doing this for 18 months now and it's generally felt fine. Hope that helps you decide on how you want to do things on your end, or anyone else has any suggestions. At a minimum, you really just need to do the createQueryOptions step to get good ergonomics with query IMO It's just worth noting, the "API" layer I described is not necessary if everything is already through query, you can just inline the API request. Similarly, if you are already using queryOptions for everything, you do not necessarily requre a "queryKeyTree" structure, but this does help with ensuring that "nested queries" are easy to invalidate by just invalidating the top level query key. But I think the ergonomics of having these levels of abstraction make things cleaner IMO
conscious-sapphire
conscious-sapphireOP7d ago
first of all sorry for the delayed response fjsjdj got busy and in a lot of my components i need the data, although because tanstack query caches it idk if it is truly necessary. Hope you can educate me on that THANK YOU SO MUCH FOR THIS that is very clever and hood how do you deal with typing an entity that comes from a query and the one used as input for a mutation For example, when i fetch my products endpoint i get id name opening_stock stock* category_id category_name* * this is data obtained via a join, aka those are not stored in the Product table per se i have this as a the type Product but, for example, when creating a product i cant send that information, cuz it doesnt matter so, beacuse i am creating these types with zod schemas, the fields that are not needed for the mutation should just be in the schema but set as optionals? productSchema= { id: number.optional() name: string(“must have name”) opening_stock: number(“must have opening stock”) stock*: number.optional() category_id: number(“must have category id”) category_name: number.optional() } type Product = z.infer<typeof productSchema> Djjsjd sorry if it sounds stupid but i really cant get a clear answer for it
quickest-silver
quickest-silver7d ago
Personally, my mutations and queries have almost completely unrelated typings. Sometimes sharing a type is useful, but I find a lot of the time for separate endpoints, unless they return the exact same thing, it's easier to just define the type. And for a query, your params are obviously completely different to a mutation.
// query
type ProductQueryParams = {}

const getProducts = async (params: ProductQueryParams) => {
const response = await fetch(buildMyUrl(params))
const json = await response.json();
const parsed = z.parse(json);
return parsed;
}

const createProductQuery = (params: ProductQueryParams) => {
return queryOptions({
queryKey: myKeys.queryKey(params)
queryFn: () => getProducts(params)
})
}

// mutation
type ProductUpdate = {}

const updateProduct = async (body: ProductUpdate) => {
const response = await fetch(buildMyUrl, { method: "POST", body });
const json = await response.json();
const parsed = z.parse(json);
return parsed;
}

const myMutationOptions = mutationOptions({
mutationKey: ['updateProduct'],
mutationFn: (body: ProductUpdate) => updateProduct(body)
})
// query
type ProductQueryParams = {}

const getProducts = async (params: ProductQueryParams) => {
const response = await fetch(buildMyUrl(params))
const json = await response.json();
const parsed = z.parse(json);
return parsed;
}

const createProductQuery = (params: ProductQueryParams) => {
return queryOptions({
queryKey: myKeys.queryKey(params)
queryFn: () => getProducts(params)
})
}

// mutation
type ProductUpdate = {}

const updateProduct = async (body: ProductUpdate) => {
const response = await fetch(buildMyUrl, { method: "POST", body });
const json = await response.json();
const parsed = z.parse(json);
return parsed;
}

const myMutationOptions = mutationOptions({
mutationKey: ['updateProduct'],
mutationFn: (body: ProductUpdate) => updateProduct(body)
})
Personally, I don't define schemas for my arguments for queries/mutations, if the application is mostly type safe, then I'm happy enough with just having a type there, but you could absolutely define an input and output schema for both a query and mutation if you wanted. --- So for your situation, I would just make a separate schema for your arguments of each query/mutation and then use the type from that, instead of trying to reuse the schema for both
conscious-sapphire
conscious-sapphireOP7d ago
THANK YOU SO MUCHH ♥️

Did you find this page helpful?