Schema validation

Any well-structured API must validate incoming requests to ensure data integrity and application security.

Beyond that, schema validation allows the developer to write properly typed handlers — so that req.body, req.query, and req.params are strongly typed and predictable.

Dotpress uses the popular library Zod to provide expressive and robust schema validation with TypeScript-first support.

With Dotpress, you can define schemas inline when declaring a route. The framework will:

  • Automatically parse and validate the request
  • Reject invalid payloads with a structured 400 error
  • Infer types for your handler based on your schema
  • Prevent runtime surprises with fully typed data access

Basic usage

defineRoute({
  method: 'post',
  path: '/members',
  schema: (z) => ({
    body: z.object({
      name: z.string(),
      age: z.number().min(13),
      location: z.string().optional(),
    }),
  }),
  handler: async ({ req }) => {
    // req.body is typed as { name: string; age: number; location: string | undefined }
    return { received: req.body };
  }
});

If the client sends malformed or missing data, the framework returns an error structured as below:

// Response Status 400 Bad Request
{
  "error": "Validation failed",
  "details": [
    {
      "source": "body",
      "issues": [
        {
          "path": ["name"],
          "message": "Required",
          "code": "invalid_type"
        }
      ]
    },
    {
      "source": "body",
      "issues": [
        {
          "path": ["age"],
          "message": "Required",
          "code": "invalid_type"
        }
      ]
    },
  ]
}

Supported attributes

You can define validation schemas for:

  • body: JSON payloads in POST/PUT
  • query: URL query strings (?sort=asc)
  • params: Express route parameters (/users/:id)

Example:

schema: (z) => ({
  params: z.object({ id: z.string().uuid() }),
  query: z.object({ sort: z.enum(['asc', 'desc']) }),
  body: z.object({ email: z.string().email(), name: z.string() }),
})

Note: since URL params are always considered as strings, you can use the following shorthand syntax:

schema: (z) => ({
  params: ['orgId', 'userId'],
})

// This will type req.params as { orgId: string; userId: string }

Failed validation

If a validation fails:

  • The request is blocked before hitting your handler
  • An HTTP 400 Bad Request is returned
  • All relevant validation errors are included in the response

Optional Response Schema

You can also define a response schema to validate the output of your handler:

schema: (z) => ({
  response: z.object({ status: z.literal('ok'), userId: z.string().uuid() }),
})

If your handler returns an invalid object, the framework will throw a Zod validation error (which will occur a 500 Internal Error response) keeping your API consistent from end to end.

It may seem overkill in most case but can be useful in some situations:

  • For internal APIs where contracts between frontend/backend must be enforced.
  • For public APIs where response validation is needed to prevent missing information or exposure of personal information.

Tips

  • Use Zod functions .refine() or .transform() to apply custom logic
  • You can omit schema entirely for routes that don’t require validation or to prototype handlers
  • You can use z.never() as a response schema to generate a 204 No Content response