holden
holden2y ago

Validating id/schema values client side?

I have a NextJS route like app/project/[id], so my id comes in from a URL param. I want to render a 404 page if the id isn't valid. The docs suggest casting on the client, like this:
const project = useQuery(api.projects.get, { id: projectId as Id<"projects"> });
const project = useQuery(api.projects.get, { id: projectId as Id<"projects"> });
This correctly throws if the string isn't a valid id. Should I just catch this on the client to render a 404? Or is there some way to run a validator in a client component? I was imagining something like zod.safeParse where I could test if a string is a valid Id<"projects">.
18 Replies
ian
ian2y ago
There are two types of validation here: 1. Is the value the right format of string 2. Is the value pointing to a valid document It's not currently easy to validate (1) on the client today, and (2) has to happen on the server, so I would suggest validating in the server function, and rendering the 404 based on the response. This way you could also catch the (2) case which is just as important - when you pass around IDs in strings, you probably want a way to eventually remove / delete a resource pointed to by the URL (e.g. if the URL gets leaked)
holden
holdenOP2y ago
Thanks, that makes sense! I'm still trying to figure out how to use Convex (following your docs/tutorials). I was previously using a NextJS server action to query Postgres in a server component, so I could do what you're suggesting and 404 if validation failed (for whatever reason). I'm trying to tear that out and move to Convex. Do you have any pointer to how I can query Convex in a server component / SSR context? I've looked around in your docs, but they generally use the useQuery/useMutation hooks to query from client components. Any pointer to a doc/example would be great!
ian
ian2y ago
ConvexHttpClient is the ticket for SSR
holden
holdenOP2y ago
Ok thanks, I can try going that route. I'm using Clerk for auth and have it working client side (following your docs/example). Any tip on how to use auth in SSR? I'm calling setAuth with my Clerk token, but getting this error when I query:
No description
holden
holdenOP2y ago
Here's the code that's not working. setAuth works, but then client.query throws.
No description
ian
ian2y ago
You probably need to await setAuth
holden
holdenOP2y ago
Tried that, but no luck (looks like it isn't async)
ballingt
ballingt2y ago
Try adding an argument to the await getToken() The way we call this on the client side includes template: "convex", see https://github.com/get-convex/convex-js/blob/main/src/react-clerk/ConvexProviderWithClerk.tsx#L63-L66
holden
holdenOP2y ago
Nice, adding the template param worked!
const token = await getToken({
template: "convex",
});
const token = await getToken({
template: "convex",
});
ballingt
ballingt2y ago
We'll get some docs and an example project up showing SSR with Next before long, thanks for pointing this out!
holden
holdenOP2y ago
That would be helpful, thanks! You might also consider exporting ArgumentValidationError if you want devs to be able to handle errors like this when validation fails.
ballingt
ballingt2y ago
@holden could you say more here, you'd want to catch and identify it? Currently this isn't a custom error class and in general propagating errors across the server/client boundary could be error-prone but we certainly want to provide everything a developer would need here to handler errors
holden
holdenOP2y ago
Sure, snippet of code below. What I'm trying to do is show a 404 page if a user tries to load /project/[id] with an invalid id or for a doc that doesn't exist or they can't access. Maybe there’s a better way to do this? I think the root cause / annoying part of this experience is that validation errors throw, rather than returning something I can handle. In async data fetching libraries like useSWR or react-query, they deal with this by returning an object like {data, isLoading, error}. That makes it very easy to render something different for the loading/error states. Throwing is especially annoying in client-side hooks, because (AFAIK) you can’t catch them without violating the rules of hooks, and dealing with an ErrorBoundary just to know if an API call succeeded isn’t great. In this case, I don’t particularly care where I deal with the error. Could be client or server. This just feels like I'm jumping through a few too many hoops to do a simple “if validation fails, render 404”. But very possible I'm missing something about Convex!
holden
holdenOP2y ago
No description
holden
holdenOP2y ago
(your support has been great btw, so thanks for that!)
ballingt
ballingt2y ago
A helper hook that returns const {data, isLoading, error} = useQueryObject() is something @MapleLeaf 🍁 has asked for this too, the simple case of data | undefined is staying but something like this is possible in user space now by wrapping useQueries — and one will end up in convex-helpers or core before long, I agree this is often the pattern I want. The general ArgumentValidationError is tough because you don't have enough information to know what the problem was if there are multiple arguments. So you probably want to accept the id as a string and then validate it with db.normalizeId(idString) and check if that's null, then return a union. Note that in production you won't get these error messages or stack traces, it's just going to say "Server error". We have some plans for improving this, but for now you want to return a union of {data: ...} and {error: "That project does not exist"}.
holden
holdenOP2y ago
Makes sense, a simple helper or hook wrapper might go a long way here. I hadn't understood the db.normalizeId thing, but good tip. I ended up just adding this to my query:
const id = ctx.db.normalizeId("projects", args.id);
if (id === null) {
return null;
}
const id = ctx.db.normalizeId("projects", args.id);
if (id === null) {
return null;
}
Doesn't throw anymore, or need a union and just treats invalid ids the same as the doc not being found. Only downside is now my id is a string instead of a typesafe id. I ended up factoring out a util to access the convex client in SSR contexts:
/**
* Return a ConvexHttpClient that can be used in server-side contexts.
*/
export async function getConvex(): Promise<ConvexHttpClient> {
const { userId, getToken } = auth();
if (!userId) {
// Clerk middleware should handle this for us.
throw new Error("Unexpected: user not signed in");
}

const token = await getToken({
template: "convex",
});
if (!token) {
throw new Error("Unexpected: failed to get Clerk token");
}

const url = process.env.NEXT_PUBLIC_CONVEX_URL;
if (!url) {
throw new Error("Unexpected: NEXT_PUBLIC_CONVEX_URL not found: ");
}

const client = new ConvexHttpClient(url);
client.setAuth(token);

return client;
}
/**
* Return a ConvexHttpClient that can be used in server-side contexts.
*/
export async function getConvex(): Promise<ConvexHttpClient> {
const { userId, getToken } = auth();
if (!userId) {
// Clerk middleware should handle this for us.
throw new Error("Unexpected: user not signed in");
}

const token = await getToken({
template: "convex",
});
if (!token) {
throw new Error("Unexpected: failed to get Clerk token");
}

const url = process.env.NEXT_PUBLIC_CONVEX_URL;
if (!url) {
throw new Error("Unexpected: NEXT_PUBLIC_CONVEX_URL not found: ");
}

const client = new ConvexHttpClient(url);
client.setAuth(token);

return client;
}
This could also be a good candidate for a helper. e.g. one thing I really like about Clerk is how easy it is to access auth state in client components (with hooks) or on the server (simple async function). Would be nice if it was as easy to access my Convex queries/mutations from anywhere. anyway, all set here, thanks for all your help!
ballingt
ballingt2y ago
good to hear, thanks for sharing the helper!

Did you find this page helpful?