Skip to main content

Handling Parameters in Snitch

This tutorial covers how to define, access, and validate different types of parameters in Snitch applications. Parameters are a crucial part of building APIs as they allow your endpoints to receive and process various kinds of input data.

Parameter Types

Snitch supports several types of parameters:

  1. Path Parameters: Variables embedded in the URL path
  2. Query Parameters: Key-value pairs in the URL query string
  3. Header Parameters: HTTP request headers
  4. Body Parameters: Data in the request body (typically JSON)

Let's explore how to use each type of parameter in your Snitch applications.

Defining Parameters

Parameters in Snitch are defined using property delegates, making them type-safe and easy to use.

Path Parameters

Path parameters are defined using the path() delegate and are embedded directly in the URL path:

// Define a path parameter
val userId by path(ofInt, description = "User ID")

// Use it in a route
GET("users" / userId) isHandledBy {
// Handler code
}

The ofInt parameter is a validator that ensures the parameter can be parsed as an integer.

Query Parameters

Query parameters are defined using the query() delegate:

// Define a query parameter
val page by query(ofInt, description = "Page number")
val limit by optionalQuery(ofInt, default = 10, description = "Items per page")

// Use it in a route
GET("users") withQuery page isHandledBy {
// Handler code
}

Header Parameters

Header parameters are defined using the header() delegate:

// Define a header parameter
val authorization by header(description = "Authorization token")

// Use it in a route
GET("secured") withHeader authorization isHandledBy {
// Handler code
}

Body Parameters

Body parameters are defined directly in the route definition:

// Define a route with a body parameter
POST("users") with body<CreateUserRequest>() isHandledBy {
// Handler code
}

Accessing Parameters in Handlers

Once parameters are defined, you can access them in your handlers using the request object.

Path Parameters

Access path parameters using indexed syntax:

val userId by path(ofInt)

val getUser by handling {
val id = request[userId]
userService.getUser(id)?.ok ?: "User not found".notFound()
}

Query Parameters

Query parameters are accessed the same way:

val page by query(ofInt)
val limit by optionalQuery(ofInt, default = 10)

val listUsers by handling {
val pageNum = request[page]
val pageSize = request[limit]
userService.getUsers(pageNum, pageSize).ok
}

Header Parameters

Headers are accessed using the same syntax:

val authorization by header()

val securedEndpoint by handling {
val authHeader = request[authorization]
if (isValidToken(authHeader)) {
"Authenticated".ok
} else {
"Unauthorized".unauthorized()
}
}

Body Parameters

Body parameters are accessed using the body property:

val createUser by parsing<CreateUserRequest>() handling {
val name = body.name
val email = body.email
userService.createUser(name, email)
"User created".created
}

Optional vs Required Parameters

By default, parameters defined with path(), query(), and header() are required. If a required parameter is missing or invalid, Snitch automatically returns a 400 Bad Request response.

For optional parameters, use the optionalPath(), optionalQuery(), and optionalHeader() delegates:

// Required parameter
val userId by path(ofInt)

// Optional parameter with default value
val page by optionalQuery(ofInt, default = 1)

// Optional parameter without default (will be null if missing)
val filter by optionalQuery()

When accessing optional parameters without defaults, check for null values:

val listUsers by handling {
val filterValue = request[filter]
val users = if (filterValue != null) {
userService.getUsersWithFilter(filterValue)
} else {
userService.getAllUsers()
}
users.ok
}

Parameter Validation

Snitch provides built-in validators for common parameter types:

  • ofInt: Validates that the parameter is an integer
  • ofLong: Validates that the parameter is a long integer
  • ofDouble: Validates that the parameter is a double
  • ofBoolean: Validates that the parameter is a boolean
  • ofNonNegativeInt: Validates that the parameter is a non-negative integer
  • ofEmail: Validates that the parameter is a valid email address
  • matches(regex): Validates that the parameter matches a regular expression

You can also create custom validators:

// Custom validator for UUIDs
val ofUUID = validator { input ->
try {
UUID.fromString(input)
input
} catch (e: IllegalArgumentException) {
throw InvalidParameterException("Invalid UUID format")
}
}

// Use the custom validator
val orderId by path(ofUUID, description = "Order ID in UUID format")

Real-World Example

Here's a more complete example showing different parameter types in action:

// Parameter definitions
val userId by path(ofInt, description = "User ID")
val page by optionalQuery(ofNonNegativeInt, default = 1, description = "Page number")
val limit by optionalQuery(ofNonNegativeInt, default = 10, description = "Items per page")
val authorization by header(description = "Bearer token")

// Route definitions
val routes = routes {
// Get user by ID
GET("users" / userId) isHandledBy {
val id = request[userId]
userService.getUser(id)?.ok ?: "User not found".notFound()
}

// List users with pagination
GET("users") withQuery page withQuery limit isHandledBy {
val pageNum = request[page]
val pageSize = request[limit]
userService.getUsers(pageNum, pageSize).ok
}

// Create user with request body
POST("users") with body<CreateUserRequest>() isHandledBy {
val newUser = userService.createUser(body.name, body.email)
newUser.created
}

// Secured endpoint with authorization header
GET("secured") withHeader authorization isHandledBy {
val token = request[authorization]
if (isValidToken(token)) {
"Authenticated".ok
} else {
"Unauthorized".unauthorized()
}
}
}

Advanced Parameter Handling

Organizing Parameters

For better organization, you can group related parameters in objects:

object Paths {
val userId by path(ofInt)
val postId by path(ofInt)
}

object Queries {
val page by optionalQuery(ofNonNegativeInt, default = 1)
val limit by optionalQuery(ofNonNegativeInt, default = 10)
}

object Headers {
val authorization by header()
}

// Using organized parameters
GET("users" / Paths.userId / "posts" / Paths.postId) isHandledBy {
val userId = request[Paths.userId]
val postId = request[Paths.postId]
// ...
}

Parameter Conditions

You can add conditions to parameters that must be satisfied:

// Define a condition for token validation
val validToken = condition<String> { token ->
jwtService.validateToken(token)
}

// Apply the condition to a parameter
val accessToken by header(
condition = validToken,
name = "X-Access-Token",
description = "Valid access token"
)

Custom Parameter Processing

For complex parameter processing, you can use the parsing handler:

val createUser by parsing<CreateUserRequest>() handling {
// Validate and transform the request body
val sanitizedName = sanitizeInput(body.name)
val normalizedEmail = normalizeEmail(body.email)

// Use the processed parameters
userService.createUser(sanitizedName, normalizedEmail)
"User created".created
}

Best Practices

  1. Use Descriptive Names: Choose parameter names that clearly indicate their purpose.

  2. Add Descriptions: Include descriptions for all parameters to improve API documentation.

  3. Validate Inputs: Use appropriate validators to ensure parameters meet your requirements.

  4. Handle Errors Gracefully: Provide meaningful error messages when parameter validation fails.

  5. Group Related Parameters: Organize parameters into logical groups for better code organization.

  6. Use Appropriate Parameter Types: Choose the right parameter type (path, query, header, body) based on your API design.

  7. Make Parameters Optional When Appropriate: Don't require parameters that aren't strictly necessary.

  8. Set Sensible Defaults: Provide meaningful default values for optional parameters.

Conclusion

Snitch's parameter handling system provides a type-safe, declarative way to define and validate request parameters. By using the right parameter types and validation rules, you can create robust APIs that gracefully handle various input scenarios.