In this task, you will implement input validation for the Post
model in the API. You will use the Zod library to define validation schemas for the input data and validate the request payloads before processing them. Input validation helps ensure data integrity, improve error handling, and prevent security vulnerabilities.
Input validation is a critical aspect of building secure and robust APIs. By validating the input data, you can ensure that the data meets the expected format and constraints before processing it. This helps to ensure data integrity, improve error handling, and prevent security vulnerabilities such as SQL injection and cross-site scripting (XSS) attacks.
There are various ways to perform input validation in an API. Moreover, there are many libraries and tools available to simplify the process. In this task, you will use the Zod library to define validation schemas for the Post
model and validate the request payloads before processing them.
Zod is a TypeScript-first schema declaration and validation library that provides a simple and expressive way to define data schemas and validate data against those schemas. It allows you to define complex validation rules, handle nested data structures, and generate detailed error messages when validation fails.
Middlewares are functions that have access to the request and response objects in an API. They can perform tasks such as parsing request bodies, authenticating users, logging requests, and handling errors. In Hono.js, middlewares can intercept and process the Request and Response objects before and after the route Handler handles the request.
For example, we can write a middleware to add the “X-Response-Time” header as follows:
app.use(async (c, next) => {
const start = Date.now()
await next()
const end = Date.now()
c.res.headers.set('X-Response-Time', `${end - start}`)
})
In the code above, the middleware calculates the response time of the request. It “starts” measuring the time before calling next()
to pass control to the next middleware or route handler. After the next middleware or route handler finishes processing the request, it “ends” measuring the time and sets the “X-Response-Time” header in the response.
Notice the the following:
c
(the Context object) and next
(a function to call the next middleware or route handler).await next()
statement calls the next middleware or route handler in the chain.app.use
method.While app.use
adds a middleware to the global middleware stack, you can also add middlewares to specific routes using the router.use
method. Moreover, you can apply a middleware (or multiple ones) to a specific route handler by passing it as an argument to the route handler.
router.get('/posts', middleware1, middleware2, async (c) => {
// Route handler logic
})
First, install the Zod library using the following command:
pnpm add zod
With Zod installed, you can now define validation schemas for the Post
model.
Create a new file named src/validators/schemas.ts
and define the validation schema for the Post
model using Zod.
import { z } from "zod";
export const createPostSchema = z.object({
content: z
.string()
.min(1, "Content is required")
.max(240, "Content must be 240 characters or less"),
});
In the code above, we define a validation schema for creating a new post (createPostSchema
) that requires a content
field of type string
with a minimum length of 1 character and a maximum length of 240 characters.
Currently, the validation schemas are simple and only validate the content
field. You can add more validation rules and constraints as needed for your application.
POST /posts
route to include input validationUpdate the src/routes/posts.ts
file to include input validation for the POST
route using the Zod library and a custom middleware.
// Other imports
import { createPostSchema } from "../validators/schemas";
// GET all posts (no changes)
postsRouter.get("/posts", async (c) => {...});
// GET a specific post by ID (no changes)
postsRouter.get("/posts/:id", async (c) => {...});
// DELETE a post (no changes)
postsRouter.delete("/posts/:id", async (c) => {...});
// POST a new post with input validation
postsRouter.post(
"/posts",
async (c, next) => { // 👀 Look here
const { content } = await c.req.json();
const validationResult = createPostSchema.safeParse({ content });
if (validationResult.success) {
return await next();
} else {
return c.json({ error: validationResult.error.errors }, 400);
}
},
async (c) => {
// No changes to the code in the route handler
}
);
// PATCH (update) a post (no changes)
postsRouter.patch("/posts/:id", async (c) => {...});
export default postsRouter;
In the code above:
src/validators/schemas.ts
.POST
route using the createPostSchema
validation schema.safeParse
method of the Zod schema to validate the request payload. If the validation succeeds, the middleware calls the next
function to pass control to the route handler. If the validation fails, the middleware returns a 400 Bad Request
response with detailed error messages.Run the API and test the POST
route with valid and invalid input data to verify that the input validation is working as expected. You can try the following payload as a test input:
{
"content": ""
}
Hono provides a built-in validation middleware that simplifies input validation using Zod schemas. Update the src/routes/posts.ts
file to use the built-in validation middleware for the POST
route.
// Other imports
import { validator } from "hono/validator";
// POST a new post with input validation
postsRouter.post(
"/posts",
validator("json", (value, c) => {
const validationResult = createPostSchema.safeParse(value);
if (!validationResult.success) {
return c.json({ error: validationResult.error.errors }, 400);
}
return validationResult.data;
}),
async (c) => {
const { content } = c.req.valid("json"); // 👀 Look here
// No changes to the remaining code in the route handler
}
);
// No change to other routes
In the code above:
validator
middleware from hono/validator
.validator
middleware to validate the request payload for the POST
route using the createPostSchema
validation schema.validator
middleware takes two arguments: the type of input to validate (json
in this case) and a validation function that returns the validated data or an error response.const { content } = c.req.valid("json")
statement retrieves the validated data from the request object. The valid
method is used to access the validated data after input validation.Run the API and test the POST
route with valid and invalid input data to verify that the input validation is working as expected.
Hono provides a middleware called @hono/zod-validator
that further simplifies input validation using Zod schemas. Install the @hono/zod-validator
package using the following command:
pnpm add @hono/zod-validator
Next update the src/routes/posts.ts
file to use the zValidator
middleware for input validation.
// Other imports
- import { validator } from "hono/validator";
+ import { zValidator } from "@hono/zod-validator";
// POST a new post with input validation
postsRouter.post(
"/posts",
- validator("json", (value, c) => {
- const validationResult = createPostSchema.safeParse(value);
- if (!validationResult.success) {
- return c.json({ error: validationResult.error.errors }, 400);
- }
- return validationResult.data;
- }),
+ zValidator("json", createPostSchema),
async (c) => {
const { content } = c.req.valid("json");
// No changes to the remaining code in the route handler
}
);
// No change to other routes
In the code above:
zValidator
middleware from @hono/zod-validator
.zValidator
middleware to validate the request payload for the POST
route using the createPostSchema
validation schema.zValidator
middleware automatically validates the request payload against the specified schema and returns a 400 Bad Request
response with detailed error messages if the validation fails. We only need to provide the type of input to validate (json
in this case) and the validation schema (createPostSchema
).Run the API and test the POST
route with valid and invalid input data to verify that the input validation is working as expected.
Update the src/validators/schemas.ts
file to include a partial validation schema for updating an existing post.
export const updatePostSchema = createPostSchema.partial();
In the code above, we define a partial validation schema for updating an existing post (updatePostSchema
) that allows updating the content
field. The partial()
method allows the schema to accept partial data, meaning that only the specified fields are validated while other fields are ignored.
PATCH /posts/:id
route to include input validation// Other imports
import { updatePostSchema } from "../validators/schemas";
// No changes to the GET, DELETE, and POST routes
// PATCH (update) a post with input validation
postsRouter.patch(
"/posts/:id",
zValidator("json", updatePostSchema), // 👀 Look here
async (c) => {
const body = c.req.valid("json"); // 👀 Look here
// No changes to the remaining code in the route handler
}
);
export default postsRouter;
In the code above:
We import the the updatePostSchema
validation schema from src/validators/schemas.ts
.
We use the zValidator
middleware to validate the request payloads for the PATCH
route. Notice that the middleware is placed before the route handler to validate the request payload before processing it.
Run the API and test the POST
and PATCH
routes with valid and invalid input data to verify that the input validation is working as expected. You can try the following Dijkstra’s quote as a test input:
{
"content": "Let me try to explain to you, what to my taste is characteristic for all intelligent thinking. It is, that one is willing to study in depth an aspect of one's subject matter in isolation for the sake of its own consistency, all the time knowing that one is occupying oneself only with one of the aspects. We know that a program must be correct and we can study it from that viewpoint only; we also know that it should be efficient and we can study its efficiency on another day, so to speak. In another mood we may ask ourselves whether, and if so: why, the program is desirable. But nothing is gained—on the contrary!—by tackling these various aspects simultaneously. It is what I sometimes have called \"the separation of concerns\", which, even if not perfectly possible, is yet the only available technique for effective ordering of one's thoughts, that I know of. This is what I mean by \"focusing one's attention upon some aspect\": it does not mean ignoring the other aspects, it is just doing justice to the fact that from this aspect's point of view, the other is irrelevant. It is being one- and multiple-track minded simultaneously."
}
We can also validate path parameters using the Zod library. Update the src/validators/schemas.ts
file to include a validation schema for the id
path parameter.
export const getPostSchema = z.object({
id: z.coerce.number().int().positive(),
});
In the code above, we define a validation schema for the id
path parameter (getPostSchema
) that requires a positive integer value. The coerce.number().int().positive()
chain of methods ensures that the value is coerced to a number, an integer, and a positive number.
Next, update the src/routes/posts.ts
file to include input validation for the routes that use path parameters.
// Other imports
import { getPostSchema } from "../validators/schemas";
// GET all posts (no changes)
postsRouter.get("/posts", async (c) => {
// No changes to the code in the route handler
});
// GET a specific post by ID
postsRouter.get(
"/posts/:id",
zValidator("param", getPostSchema), // 👀 Look here
async (c) => {
const { id } = c.req.valid("param"); // 👀 Look here
// No changes to the remaining code in the route handler
}
);
// DELETE a post
postsRouter.delete(
"/posts/:id",
zValidator("param", getPostSchema), // 👀 Look here
async (c) => {
const { id } = c.req.valid("param"); // 👀 Look here
// No changes to the remaining code in the route handler
}
);
// POST a new post with input validation (no changes)
postsRouter.post(
"/posts",
zValidator("json", createPostSchema),
async (c) => {
const { content } = c.req.valid("json");
// No changes to the remaining code in the route handler
}
);
// PATCH (update) a post (no changes)
postsRouter.patch(
"/posts/:id",
zValidator("param", getPostSchema), // 👀 Look here
zValidator("json", updatePostSchema),
async (c) => {
const { id } = c.req.valid("param"); // 👀 Look here
const { content } = c.req.valid("json");
// No changes to the remaining code in the route handler
}
);
export default postsRouter;
In the code above:
getPostSchema
validation schema from src/validators/schemas.ts
.zValidator
middleware to validate the path parameters for the GET
, DELETE
, and PATCH
routes that use the id
path parameter. The middleware is placed before the route handler to validate the path parameters before processing the request.Run the API and test the GET
, DELETE
, and PATCH
routes with valid and invalid path parameters to verify that the input validation is working as expected.
In this task, you learned how to implement input validation for the Post
model in the API using the Zod library. You defined validation schemas for creating and updating posts, validated request payloads using custom middleware, Hono’s built-in validation middleware, and the @hono/zod-validator
middleware. You also learned how to validate path parameters using Zod schemas. Input validation is an essential part of building secure and robust APIs, and using libraries like Zod can simplify the process and improve data integrity and error handling.