0
# Query Parameter Processing
1
2
Type-safe query parameter extraction and validation with support for optional parameters, multi-value parameters, custom decoders, and validation with detailed error reporting.
3
4
## Capabilities
5
6
### Query Parameter Extraction
7
8
#### Basic Query Parameter Extractor (:?)
9
10
Fundamental query parameter extractor that provides access to all query parameters.
11
12
```scala { .api }
13
/**
14
* Query parameter extractor
15
* Extracts request and its query parameters as a Map
16
*/
17
object :? {
18
def unapply[F[_]](req: Request[F]): Some[(Request[F], Map[String, collection.Seq[String]])]
19
}
20
```
21
22
**Usage Examples:**
23
24
```scala
25
val routes = HttpRoutes.of[IO] {
26
// Access all query parameters
27
case GET -> Root / "search" :? params =>
28
val query = params.get("q").flatMap(_.headOption).getOrElse("")
29
Ok(s"Search query: $query")
30
31
// Pattern match with specific extractors
32
case GET -> Root / "users" :? Limit(limit) +& Offset(offset) =>
33
Ok(s"Limit: $limit, Offset: $offset")
34
}
35
```
36
37
#### Parameter Combination (+&)
38
39
Combinator for extracting multiple query parameters from the same request.
40
41
```scala { .api }
42
/**
43
* Multiple parameter extractor combinator
44
* Allows chaining multiple parameter extractors
45
*/
46
object +& {
47
def unapply(params: Map[String, collection.Seq[String]]):
48
Some[(Map[String, collection.Seq[String]], Map[String, collection.Seq[String]])]
49
}
50
```
51
52
**Usage Examples:**
53
54
```scala
55
object Limit extends QueryParamDecoderMatcher[Int]("limit")
56
object Offset extends QueryParamDecoderMatcher[Int]("offset")
57
object SortBy extends QueryParamDecoderMatcher[String]("sort")
58
59
val routes = HttpRoutes.of[IO] {
60
// Combine multiple parameters
61
case GET -> Root / "users" :? Limit(limit) +& Offset(offset) =>
62
Ok(s"Pagination: limit=$limit, offset=$offset")
63
64
// Chain many parameters
65
case GET -> Root / "search" :? Query(q) +& Limit(limit) +& SortBy(sort) =>
66
Ok(s"Search: $q, limit: $limit, sort: $sort")
67
}
68
```
69
70
### Query Parameter Matchers
71
72
#### Basic Query Parameter Decoder Matcher
73
74
Type-safe query parameter extraction with automatic type conversion.
75
76
```scala { .api }
77
/**
78
* Basic query parameter matcher with type conversion
79
* Uses QueryParamDecoder for type-safe conversion
80
*/
81
abstract class QueryParamDecoderMatcher[T: QueryParamDecoder](name: String) {
82
def unapply(params: Map[String, collection.Seq[String]]): Option[T]
83
def unapplySeq(params: Map[String, collection.Seq[String]]): Option[collection.Seq[T]]
84
}
85
```
86
87
**Usage Examples:**
88
89
```scala
90
// Define parameter matchers
91
object UserId extends QueryParamDecoderMatcher[Int]("user_id")
92
object Active extends QueryParamDecoderMatcher[Boolean]("active")
93
object Tags extends QueryParamDecoderMatcher[String]("tags")
94
95
val routes = HttpRoutes.of[IO] {
96
// Single value extraction
97
case GET -> Root / "user" :? UserId(id) =>
98
Ok(s"User ID: $id")
99
100
// Boolean parameters
101
case GET -> Root / "users" :? Active(isActive) =>
102
Ok(s"Active users only: $isActive")
103
104
// Multiple values of same parameter
105
case GET -> Root / "posts" :? Tags.unapplySeq(tags) =>
106
Ok(s"Tags: ${tags.mkString(", ")}")
107
}
108
```
109
110
#### Query Parameter Matcher with Implicit QueryParam
111
112
Simplified matcher that uses implicit QueryParam for parameter name resolution.
113
114
```scala { .api }
115
/**
116
* Query parameter matcher using implicit QueryParam for name resolution
117
*/
118
abstract class QueryParamMatcher[T: QueryParamDecoder: QueryParam]
119
extends QueryParamDecoderMatcher[T](QueryParam[T].key.value)
120
```
121
122
**Usage Examples:**
123
124
```scala
125
// Define parameter with implicit QueryParam
126
case class Limit(value: Int)
127
implicit val limitParam: QueryParam[Limit] = QueryParam.fromKey("limit")
128
implicit val limitDecoder: QueryParamDecoder[Limit] =
129
QueryParamDecoder[Int].map(Limit.apply)
130
131
object LimitMatcher extends QueryParamMatcher[Limit]
132
133
val routes = HttpRoutes.of[IO] {
134
case GET -> Root / "data" :? LimitMatcher(limit) =>
135
Ok(s"Limit: ${limit.value}")
136
}
137
```
138
139
### Optional Query Parameters
140
141
#### Optional Query Parameter Decoder Matcher
142
143
Handle optional query parameters that may or may not be present.
144
145
```scala { .api }
146
/**
147
* Optional query parameter matcher
148
* Returns Some(Some(value)) if parameter present and valid
149
* Returns Some(None) if parameter absent
150
* Returns None if parameter present but invalid
151
*/
152
abstract class OptionalQueryParamDecoderMatcher[T: QueryParamDecoder](name: String) {
153
def unapply(params: Map[String, collection.Seq[String]]): Option[Option[T]]
154
}
155
```
156
157
**Usage Examples:**
158
159
```scala
160
object OptionalLimit extends OptionalQueryParamDecoderMatcher[Int]("limit")
161
object OptionalSort extends OptionalQueryParamDecoderMatcher[String]("sort")
162
163
val routes = HttpRoutes.of[IO] {
164
case GET -> Root / "users" :? OptionalLimit(limitOpt) +& OptionalSort(sortOpt) =>
165
val limit = limitOpt.getOrElse(10)
166
val sort = sortOpt.getOrElse("name")
167
Ok(s"Limit: $limit, Sort: $sort")
168
}
169
```
170
171
#### Query Parameter Matcher with Default Value
172
173
Provide default values for missing query parameters.
174
175
```scala { .api }
176
/**
177
* Query parameter matcher with default value
178
* Returns default value if parameter is missing
179
* Returns None if parameter is present but invalid
180
*/
181
abstract class QueryParamDecoderMatcherWithDefault[T: QueryParamDecoder](name: String, default: T) {
182
def unapply(params: Map[String, collection.Seq[String]]): Option[T]
183
}
184
185
abstract class QueryParamMatcherWithDefault[T: QueryParamDecoder: QueryParam](default: T)
186
extends QueryParamDecoderMatcherWithDefault[T](QueryParam[T].key.value, default)
187
```
188
189
**Usage Examples:**
190
191
```scala
192
object LimitWithDefault extends QueryParamDecoderMatcherWithDefault[Int]("limit", 10)
193
object PageWithDefault extends QueryParamDecoderMatcherWithDefault[Int]("page", 1)
194
195
val routes = HttpRoutes.of[IO] {
196
case GET -> Root / "data" :? LimitWithDefault(limit) +& PageWithDefault(page) =>
197
Ok(s"Page: $page, Limit: $limit")
198
// ?limit=20&page=3 -> "Page: 3, Limit: 20"
199
// ?page=2 -> "Page: 2, Limit: 10" (default limit)
200
// (no params) -> "Page: 1, Limit: 10" (both defaults)
201
}
202
```
203
204
### Multi-Value Query Parameters
205
206
#### Optional Multi-Value Query Parameter Matcher
207
208
Handle query parameters that can appear multiple times.
209
210
```scala { .api }
211
/**
212
* Multi-value query parameter matcher
213
* Handles parameters that appear multiple times in the query string
214
* Returns Validated result with all values or parse errors
215
*/
216
abstract class OptionalMultiQueryParamDecoderMatcher[T: QueryParamDecoder](name: String) {
217
def unapply(params: Map[String, collection.Seq[String]]):
218
Option[ValidatedNel[ParseFailure, List[T]]]
219
}
220
```
221
222
**Usage Examples:**
223
224
```scala
225
import cats.data.Validated
226
import cats.data.Validated.{Invalid, Valid}
227
228
object Tags extends OptionalMultiQueryParamDecoderMatcher[String]("tag")
229
object Ids extends OptionalMultiQueryParamDecoderMatcher[Int]("id")
230
231
val routes = HttpRoutes.of[IO] {
232
// Handle multiple tag parameters: ?tag=scala&tag=http4s&tag=web
233
case GET -> Root / "posts" :? Tags(tagResult) =>
234
tagResult match {
235
case Valid(tags) => Ok(s"Tags: ${tags.mkString(", ")}")
236
case Invalid(errors) => BadRequest(s"Invalid tags: ${errors.toList.mkString(", ")}")
237
}
238
239
// Multiple ID parameters: ?id=1&id=2&id=3
240
case GET -> Root / "users" :? Ids(idResult) =>
241
idResult match {
242
case Valid(ids) => Ok(s"User IDs: ${ids.mkString(", ")}")
243
case Invalid(_) => BadRequest("Invalid user IDs")
244
}
245
}
246
```
247
248
### Validating Query Parameters
249
250
#### Validating Query Parameter Matcher
251
252
Get detailed validation results with error information.
253
254
```scala { .api }
255
/**
256
* Validating query parameter matcher
257
* Returns validation result with detailed error information
258
*/
259
abstract class ValidatingQueryParamDecoderMatcher[T: QueryParamDecoder](name: String) {
260
def unapply(params: Map[String, collection.Seq[String]]):
261
Option[ValidatedNel[ParseFailure, T]]
262
}
263
```
264
265
**Usage Examples:**
266
267
```scala
268
import cats.data.Validated.{Invalid, Valid}
269
270
object ValidatingAge extends ValidatingQueryParamDecoderMatcher[Int]("age")
271
object ValidatingEmail extends ValidatingQueryParamDecoderMatcher[String]("email")
272
273
val routes = HttpRoutes.of[IO] {
274
case GET -> Root / "user" :? ValidatingAge(ageResult) =>
275
ageResult match {
276
case Valid(age) if age >= 0 && age <= 150 =>
277
Ok(s"Valid age: $age")
278
case Valid(age) =>
279
BadRequest(s"Age out of range: $age")
280
case Invalid(errors) =>
281
BadRequest(s"Invalid age format: ${errors.head.sanitized}")
282
}
283
}
284
```
285
286
#### Optional Validating Query Parameter Matcher
287
288
Combine optional parameters with validation.
289
290
```scala { .api }
291
/**
292
* Optional validating query parameter matcher
293
* Returns None if parameter absent, Some(validation result) if present
294
*/
295
abstract class OptionalValidatingQueryParamDecoderMatcher[T: QueryParamDecoder](name: String) {
296
def unapply(params: Map[String, collection.Seq[String]]):
297
Some[Option[ValidatedNel[ParseFailure, T]]]
298
}
299
```
300
301
**Usage Examples:**
302
303
```scala
304
object OptionalValidatingLimit extends OptionalValidatingQueryParamDecoderMatcher[Int]("limit")
305
306
val routes = HttpRoutes.of[IO] {
307
case GET -> Root / "data" :? OptionalValidatingLimit(limitOpt) =>
308
limitOpt match {
309
case None => Ok("No limit specified")
310
case Some(Valid(limit)) if limit > 0 => Ok(s"Limit: $limit")
311
case Some(Valid(limit)) => BadRequest("Limit must be positive")
312
case Some(Invalid(errors)) => BadRequest(s"Invalid limit: ${errors.head.sanitized}")
313
}
314
}
315
```
316
317
### Flag Query Parameters
318
319
#### Boolean Flag Query Parameter Matcher
320
321
Handle boolean flag parameters (present/absent).
322
323
```scala { .api }
324
/**
325
* Boolean flag query parameter matcher
326
* Returns true if parameter is present (regardless of value)
327
* Returns false if parameter is absent
328
*/
329
abstract class FlagQueryParamMatcher(name: String) {
330
def unapply(params: Map[String, collection.Seq[String]]): Option[Boolean]
331
}
332
```
333
334
**Usage Examples:**
335
336
```scala
337
object DebugFlag extends FlagQueryParamMatcher("debug")
338
object VerboseFlag extends FlagQueryParamMatcher("verbose")
339
340
val routes = HttpRoutes.of[IO] {
341
case GET -> Root / "status" :? DebugFlag(debug) +& VerboseFlag(verbose) =>
342
val message = (debug, verbose) match {
343
case (true, true) => "Debug and verbose mode enabled"
344
case (true, false) => "Debug mode enabled"
345
case (false, true) => "Verbose mode enabled"
346
case (false, false) => "Normal mode"
347
}
348
Ok(message)
349
// ?debug -> debug=true, verbose=false
350
// ?debug&verbose -> debug=true, verbose=true
351
// (no flags) -> debug=false, verbose=false
352
}
353
```
354
355
### Custom Query Parameter Decoders
356
357
You can create custom decoders for complex parameter types:
358
359
```scala
360
// Custom case class
361
case class SortOrder(field: String, direction: String)
362
363
// Custom decoder
364
implicit val sortOrderDecoder: QueryParamDecoder[SortOrder] =
365
QueryParamDecoder[String].emap { str =>
366
str.split(":") match {
367
case Array(field, direction) if Set("asc", "desc").contains(direction) =>
368
Right(SortOrder(field, direction))
369
case _ =>
370
Left(ParseFailure("Invalid sort format, expected 'field:direction'", ""))
371
}
372
}
373
374
object SortOrderParam extends QueryParamDecoderMatcher[SortOrder]("sort")
375
376
val routes = HttpRoutes.of[IO] {
377
// ?sort=name:asc or ?sort=date:desc
378
case GET -> Root / "users" :? SortOrderParam(sort) =>
379
Ok(s"Sort by ${sort.field} ${sort.direction}")
380
}
381
```