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:
- Path Parameters: Variables embedded in the URL path
- Query Parameters: Key-value pairs in the URL query string
- Header Parameters: HTTP request headers
- 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 integerofLong
: Validates that the parameter is a long integerofDouble
: Validates that the parameter is a doubleofBoolean
: Validates that the parameter is a booleanofNonNegativeInt
: Validates that the parameter is a non-negative integerofEmail
: Validates that the parameter is a valid email addressmatches(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
-
Use Descriptive Names: Choose parameter names that clearly indicate their purpose.
-
Add Descriptions: Include descriptions for all parameters to improve API documentation.
-
Validate Inputs: Use appropriate validators to ensure parameters meet your requirements.
-
Handle Errors Gracefully: Provide meaningful error messages when parameter validation fails.
-
Group Related Parameters: Organize parameters into logical groups for better code organization.
-
Use Appropriate Parameter Types: Choose the right parameter type (path, query, header, body) based on your API design.
-
Make Parameters Optional When Appropriate: Don't require parameters that aren't strictly necessary.
-
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.