13.3 - Validating Inputs and Throwing Errors
In tRPC, ensuring that the data sent to your API is correct and safe is crucial. This is where input validation comes into play. tRPC integrates seamlessly with Zod, a TypeScript-first schema declaration and validation library, to enforce data validation on your procedures. Alongside this, you'll want to handle errors effectively to provide meaningful feedback and ensure the stability of your application.
Input Validation
Why Input Validation is Important
Input validation helps you:
- Prevent Invalid Data: By ensuring that only valid data reaches your server, you reduce the risk of bugs, crashes, and security vulnerabilities.
- Improve Data Integrity: Consistent data validation ensures that your database remains in a valid state, free of corrupt or malformed data.
- Provide Better User Feedback: When inputs are validated, you can provide users with clear, actionable error messages if something goes wrong.
Using Zod for Input Validation
Zod is a powerful library for defining schemas and validating data in TypeScript. In tRPC, you can use Zod schemas to validate the inputs to your procedures. Let’s see how it works with an example.
Example: Validating Input with Zod
Suppose we have a procedure for creating a new user. We want to ensure that the input data (like username and email) is in the correct format before processing it:
import { z } from "zod";
const userRouter = router({
createUser: procedure
.input(
z.object({
username: z
.string()
.min(3, "Username must be at least 3 characters long"),
email: z.string().email("Invalid email address"),
})
)
.mutation(({ input }) => {
// Logic to create a new user
return createUserInDatabase(input);
}),
});
In this example:
- The
username
must be a string with at least 3 characters. - The
email
must be a valid email address format. - If the input does not meet these criteria, Zod will automatically throw a validation error, preventing the procedure from executing with invalid data.
Throwing Custom Errors
In addition to validation errors, you may want to throw custom errors in certain situations, such as when a database operation fails or a resource is not found.
Example: Throwing Custom Errors
Let’s enhance our getUser
procedure to throw an error if the user is not found:
import { TRPCError } from "@trpc/server";
const userRouter = router({
getUser: procedure.input(z.object({ id: z.string() })).query(({ input }) => {
const user = getUserFromDatabase(input.id);
if (!user) {
throw new TRPCError({
code: "NOT_FOUND",
message: `User with ID ${input.id} not found`,
});
}
return user;
}),
});
In this example:
- We use
TRPCError
to throw a custom error when the user is not found in the database. - The error includes a code (
NOT_FOUND
) and a descriptive message.
Best Practices for Input Validation and Error Handling
-
Validate All Inputs: Always validate the inputs to your procedures, especially for mutations. This ensures that your application only processes valid data.
-
Use Descriptive Error Messages: When throwing errors, provide clear and descriptive messages. This helps with debugging and improves the user experience.
-
Handle Errors Gracefully: Ensure that your application can handle errors gracefully. For example, if a user tries to access a resource that doesn’t exist, return a friendly error message rather than letting the application crash.
-
Consider Using Custom Error Codes: Use custom error codes to categorize errors, making it easier to handle them on the client side. tRPC’s
TRPCError
provides a way to define custom error codes.