Request Body and Query Parameter Validation

In this chapter, you'll learn how to validate request body and query parameters in your custom API route.

Request Validation#

Consider you're creating a POST API route at /custom/sum. It accepts two parameters a and b that are required numbers, and returns their sum.

MedusaGo provides two middlewares to validate the request body and query parameters of incoming requests to your custom API routes. They're in the framework/src/http/utils package:

  • ValidateAndTransformBody to validate the request's body parameters against a schema.
  • ValidateAndTransformQuery to validate the request's query parameters against a schema.

Both middlewares accept a schema from the framework/src/zod package. It's a Go port of Zod that has the same parsing rules as the Zod version Medusa uses, which gives you flexibility in how you define your validation schema with complex rules. It also produces the same error messages as Medusa.

The next steps explain how to add request body and query parameter validation to the API route mentioned earlier.


How to Validate Request Body#

Step 1: Create Validation Schema#

To create a validation schema, use the functions of the zod package, which are the Go equivalents of Zod's builders. Define the schemas of your API routes in a Go file next to them.

For example, create the following schema:

src/api/custom/validators.go
// PostSumSchema accepts an object with two required numbers, a and b.
var PostSumSchema = zod.Object(
	zod.F("a", zod.Number()),
	zod.F("b", zod.Number()),
)

The PostSumSchema variable is a schema that indicates the request body is valid if:

  1. It's an object.
  2. It has a property a that is a required number.
  3. It has a property b that is a required number.
Note: An object schema at the top level of a request body or query is strict: the request is invalid if it has a property that the schema doesn't declare.

Step 2: Add Request Body Validation Middleware#

To use this schema for validating the body parameters of requests to /custom/sum, use the ValidateAndTransformBody middleware. It accepts a function that returns the schema, since the schema can depend on the request. For a static schema, wrap it with the httputils.Schema helper.

For example, add the following middleware configuration to the Middlewares of your project:

src/api/middlewares.go
// SumMiddlewares validate the body of POST requests and the query parameters
// of GET requests to /custom/sum.
var SumMiddlewares = []medusahttp.MiddlewareRoute{
	{
		Matcher: "/custom/sum",
		Methods: []string{"POST"},
		Middlewares: []medusahttp.MiddlewareFunction{
			httputils.ValidateAndTransformBody(httputils.Schema(PostSumSchema)),
		},
	},
	{
		Matcher: "/custom/sum",
		Methods: []string{"GET"},
		Middlewares: []medusahttp.MiddlewareFunction{
			httputils.ValidateAndTransformQuery(GetSumSchema, httputils.QueryConfig{}),
		},
	},
}

This applies the ValidateAndTransformBody middleware on POST requests to /custom/sum. It uses the PostSumSchema as the validation schema.

How the Validation Works

If a request's body parameters don't pass the validation, the ValidateAndTransformBody middleware returns an error indicating the validation errors.

If a request's body parameters are validated successfully, the middleware sets the validated body parameters in the ValidatedBody property of MedusaRequest.

Step 3: Use Validated Body in API Route#

In your API route, consume the validated body using the ValidatedBody property of MedusaRequest.

For example, create the following handlers:

src/api/custom/sum/route.go
func sumPOST(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
	// ValidatedBody holds the output of the schema: an object whose
	// properties have the types the schema declares.
	validated := body(req.ValidatedBody)

	return res.JSON(map[string]any{
		"sum": numberOf(validated, "a") + numberOf(validated, "b"),
	})
}

func sumGET(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
	return res.JSON(map[string]any{
		"sum": numberOf(req.ValidatedQuery, "a") + numberOf(req.ValidatedQuery, "b"),
	})
}

ValidatedBody holds the output of the schema. For an object schema, that's a *dal.Record whose values have the types the schema declares: a number is a float64, a string is a string, an array is a []any, and a nested object is a *dal.Record. The body and numberOf functions in this example are helpers that read a record's values.

Convert the Validated Body into a Go Type#

To work with a Go struct instead of a record, marshal the validated body to JSON and unmarshal it into a type:

src/api/custom/posts/route.go
// CreatePostInput is the type of the validated body of CreatePostSchema.
type CreatePostInput struct {
	Title       string         `json:"title"`
	AuthorEmail string         `json:"author_email"`
	Status      string         `json:"status"`
	Tags        []string       `json:"tags"`
	Metadata    map[string]any `json:"metadata"`
}

// decode converts a validated value into a Go type.
func decode[T any](validated any) (T, error) {
	var out T

	raw, err := dal.MarshalValue(validated)
	if err != nil {
		return out, err
	}

	err = json.Unmarshal(raw, &out)
	return out, err
}

func createPostPOST(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
	input, err := decode[CreatePostInput](req.ValidatedBody)
	if err != nil {
		return err
	}

	return res.Status(201).JSON(map[string]any{
		"title":  input.Title,
		"status": input.Status,
	})
}

Test it Out#

To test out the validation, send a POST request to /custom/sum passing a and b body parameters. You can try sending incorrect request body parameters to test out the validation.

For example, if you omit the a parameter, you'll receive a 400 response code with the following response data:

Code
{
  "type": "invalid_data",
  "message": "Invalid request: Field 'a' is required"
}

If you pass a value of the wrong type, or a property that the schema doesn't declare, the message is, respectively:

Code
Invalid request: Expected type: 'number' for field 'a', got: 'x'
Invalid request: Unrecognized fields: 'c'

How to Validate Request Query Parameters#

The steps to validate the request query parameters are similar to that of validating the body.

Step 1: Create Validation Schema#

The first step is to create a schema with the rules of the accepted query parameters.

Consider that the API route accepts two query parameters a and b that are numbers, similar to the previous section.

Create the following schema:

src/api/custom/validators.go
// GetSumSchema accepts the query parameters a and b. Query parameters are
// strings, so they are converted into numbers before they're validated.
var GetSumSchema = zod.Object(
	zod.F("a", zod.Preprocess(parseNumber, zod.Number())),
	zod.F("b", zod.Preprocess(parseNumber, zod.Number())),
)

Since a query parameter's type is originally a string or a slice of strings, you have to use the Preprocess function to validate other query types, such as numbers.

Preprocess accepts a function and a schema. For both a and b, the function transforms the query parameter's value to a number first if it's a string, then the schema checks that the resulting value is a number. The parseNumber function in this example uses strconv.ParseFloat:

src/api/helpers.go
// parseNumber converts a query string value into a number, leaving other
// values untouched so that validation reports them.
func parseNumber(v any) any {
	s, ok := v.(string)
	if !ok || strings.TrimSpace(s) == "" {
		return v
	}
	f, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
	if err != nil {
		return v
	}
	return f
}
Tip: For a query parameter that you want to convert with JavaScript's Number semantics, you can use zod.CoerceNumber() instead, which is the port of z.coerce.number().

Step 2: Add Request Query Validation Middleware#

Next, you'll use the schema to validate incoming requests' query parameters to the /custom/sum API route.

Add the ValidateAndTransformQuery middleware to the API route in the Middlewares of your project. It's in the same configuration list as the body validation middleware:

src/api/middlewares.go
// SumMiddlewares validate the body of POST requests and the query parameters
// of GET requests to /custom/sum.
var SumMiddlewares = []medusahttp.MiddlewareRoute{
	{
		Matcher: "/custom/sum",
		Methods: []string{"POST"},
		Middlewares: []medusahttp.MiddlewareFunction{
			httputils.ValidateAndTransformBody(httputils.Schema(PostSumSchema)),
		},
	},
	{
		Matcher: "/custom/sum",
		Methods: []string{"GET"},
		Middlewares: []medusahttp.MiddlewareFunction{
			httputils.ValidateAndTransformQuery(GetSumSchema, httputils.QueryConfig{}),
		},
	},
}

The ValidateAndTransformQuery accepts two parameters:

  • The first one is the schema to validate the query parameters against.
  • The second one is a QueryConfig struct with options for retrieving data using Query, which you can learn more about in this chapter. It's empty in this example. You'll use it to restrict the retrievable fields in the Allowed Fields chapter.

How the Validation Works

If a request's query parameters don't pass the validation, the ValidateAndTransformQuery middleware returns an error indicating the validation errors.

If a request's query parameters are validated successfully, the middleware sets the validated query parameters in the ValidatedQuery property of MedusaRequest, which is a *dal.Record.

Step 3: Use Validated Query in API Route#

Finally, use the validated query in the API route. ValidatedQuery has the validated parameters.

For example, the following handler is the one for GET requests, in the handlers shown earlier:

src/api/custom/sum/route.go
func sumPOST(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
	// ValidatedBody holds the output of the schema: an object whose
	// properties have the types the schema declares.
	validated := body(req.ValidatedBody)

	return res.JSON(map[string]any{
		"sum": numberOf(validated, "a") + numberOf(validated, "b"),
	})
}

func sumGET(req *medusahttp.MedusaRequest, res *medusahttp.MedusaResponse) error {
	return res.JSON(map[string]any{
		"sum": numberOf(req.ValidatedQuery, "a") + numberOf(req.ValidatedQuery, "b"),
	})
}

In the sumGET handler, you use the ValidatedQuery property of MedusaRequest to access the values of the a and b properties as numbers, then return in the response their sum.

Don't forget to add the route file to the Routes of your project:

src/api/custom/sum/route.go
// SumRoute is the route file of /custom/sum.
var SumRoute = medusahttp.RouteFile{
	Path:  "/custom/sum",
	Route: medusahttp.Route{GET: sumGET, POST: sumPOST},
}

Test it Out#

To test out the validation, send a GET request to /custom/sum with a and b query parameters. You can try sending incorrect query parameters to see how the validation works.

For example, if you omit the a parameter, you'll receive a 400 response code with the following response data:

Code
{
  "type": "invalid_data",
  "message": "Invalid request: Field 'a' is required"
}

Available Schemas#

The zod package is a port of the parts of Zod that API validation needs. These are the functions and methods you can use:

Kind

Functions and Methods

Primitives

String(), Number(), Boolean(), Null(), Unknown(), Any(), Literal(values...), Enum(values...), CoerceNumber(), CoerceDate()

Collections

Array(schema), Object(zod.F(key, schema)...), Record(keySchema, valueSchema), Union(schemas...)

Modifiers

Optional(), Nullable(), Nullish(), Default(value), Preprocess(fn, schema), Transform(fn), Refine(fn, message), SuperRefine(fn), Lazy(fn)

Checks

Min(n), Max(n), Gt(n), Gte(n), Nonnegative(), Int(), Email()

Object methods

Strict(), Passthrough(), Strip(), Extend(fields...), Merge(schema), Pick(keys...), Omit(keys...), Partial()

For example, this schema uses some of them:

src/api/custom/validators.go
// CreatePostSchema shows other common rules: optional properties, string
// length checks, enums, arrays and defaults.
var CreatePostSchema = zod.Object(
	zod.F("title", zod.String().Min(1)),
	zod.F("author_email", zod.String().Email()),
	zod.F("status", zod.Enum("draft", "published").Default("draft")),
	zod.F("tags", zod.Array(zod.String()).Optional()),
	zod.F("metadata", zod.Record(zod.String(), zod.Unknown()).Nullish()),
)

Learn more about how schemas work in Zod's documentation. The functions above behave like their Zod equivalents.

Found a problem with this page?Report Issue
Edited Nov 14·Edit this page