0
# Functional Web Framework
1
2
Spring MVC's functional web framework provides a modern, functional programming approach to web application development. It offers an alternative to annotation-based controllers using RouterFunction and HandlerFunction interfaces for composable, type-safe request routing and handling.
3
4
## Capabilities
5
6
### RouterFunction
7
8
The core functional interface that represents a function routing to a handler function. RouterFunctions can be composed, filtered, and nested to create complex routing logic in a functional style.
9
10
```java { .api }
11
/**
12
* Represents a function that routes to a handler function.
13
* @param <T> the type of response returned by the handler function
14
*/
15
@FunctionalInterface
16
public interface RouterFunction<T extends ServerResponse> {
17
18
/**
19
* Return the handler function that matches the given request, if any.
20
* @param request the request to route
21
* @return an Optional containing the handler function, or empty if no match
22
*/
23
Optional<HandlerFunction<T>> route(ServerRequest request);
24
25
/**
26
* Return a composed routing function that first tries this function, then the other.
27
* @param other the router function to apply when this function returns empty
28
* @return a composed router function
29
*/
30
default RouterFunction<T> and(RouterFunction<T> other);
31
32
/**
33
* Return a composed routing function that routes to different response types.
34
* @param other the router function to compose with
35
* @return a composed router function
36
*/
37
default RouterFunction<?> andOther(RouterFunction<?> other);
38
39
/**
40
* Return a composed routing function that tests the given predicate against the request.
41
* @param predicate the predicate to test
42
* @param handlerFunction the handler function to route to if predicate matches
43
* @return a composed router function
44
*/
45
default RouterFunction<T> andRoute(RequestPredicate predicate, HandlerFunction<T> handlerFunction);
46
47
/**
48
* Filter all handler functions routed by this function with the given filter function.
49
* @param filterFunction the filter to apply
50
* @return a filtered router function
51
*/
52
default <S extends ServerResponse> RouterFunction<S> filter(HandlerFilterFunction<T, S> filterFunction);
53
54
/**
55
* Add the given attribute to this router function.
56
* @param name the attribute name
57
* @param value the attribute value
58
* @return a router function with the added attribute
59
*/
60
default RouterFunction<T> withAttribute(String name, Object value);
61
62
/**
63
* Add the given attributes to this router function.
64
* @param attributes the attributes to add
65
* @return a router function with the added attributes
66
*/
67
default RouterFunction<T> withAttributes(Consumer<Map<String, Object>> attributesConsumer);
68
}
69
```
70
71
**Usage Examples:**
72
73
```java
74
@Configuration
75
public class RouterConfig {
76
77
@Bean
78
public RouterFunction<ServerResponse> userRoutes() {
79
return route(GET("/users"), this::getAllUsers)
80
.andRoute(GET("/users/{id}"), this::getUser)
81
.andRoute(POST("/users"), this::createUser)
82
.andRoute(PUT("/users/{id}"), this::updateUser)
83
.andRoute(DELETE("/users/{id}"), this::deleteUser);
84
}
85
86
@Bean
87
public RouterFunction<ServerResponse> productRoutes() {
88
return nest(path("/products"),
89
route(GET(""), this::getAllProducts)
90
.andRoute(GET("/{id}"), this::getProduct)
91
.andRoute(POST(""), this::createProduct)
92
).filter(this::loggingFilter);
93
}
94
95
private ServerResponse getAllUsers(ServerRequest request) {
96
// Implementation
97
return ServerResponse.ok().body(userService.findAll());
98
}
99
}
100
```
101
102
### HandlerFunction
103
104
Functional interface representing a function that handles a server request and returns a server response.
105
106
```java { .api }
107
/**
108
* Represents a function that handles a server request.
109
* @param <T> the type of the response returned by this function
110
*/
111
@FunctionalInterface
112
public interface HandlerFunction<T extends ServerResponse> {
113
114
/**
115
* Handle the given request.
116
* @param request the request to handle
117
* @return the response
118
* @throws Exception if an error occurs during handling
119
*/
120
T handle(ServerRequest request) throws Exception;
121
}
122
```
123
124
**Usage Examples:**
125
126
```java
127
public class UserHandler {
128
129
private final UserService userService;
130
131
public UserHandler(UserService userService) {
132
this.userService = userService;
133
}
134
135
public ServerResponse getAllUsers(ServerRequest request) {
136
String sort = request.queryParam("sort").orElse("name");
137
List<User> users = userService.findAll(sort);
138
return ServerResponse.ok()
139
.contentType(MediaType.APPLICATION_JSON)
140
.body(users);
141
}
142
143
public ServerResponse getUser(ServerRequest request) {
144
Long id = Long.valueOf(request.pathVariable("id"));
145
return userService.findById(id)
146
.map(user -> ServerResponse.ok()
147
.contentType(MediaType.APPLICATION_JSON)
148
.body(user))
149
.orElse(ServerResponse.notFound().build());
150
}
151
152
public ServerResponse createUser(ServerRequest request) throws Exception {
153
User user = request.body(User.class);
154
User savedUser = userService.save(user);
155
URI location = URI.create("/users/" + savedUser.getId());
156
return ServerResponse.created(location)
157
.contentType(MediaType.APPLICATION_JSON)
158
.body(savedUser);
159
}
160
161
public ServerResponse updateUser(ServerRequest request) throws Exception {
162
Long id = Long.valueOf(request.pathVariable("id"));
163
User updates = request.body(User.class);
164
return userService.update(id, updates)
165
.map(user -> ServerResponse.ok()
166
.contentType(MediaType.APPLICATION_JSON)
167
.body(user))
168
.orElse(ServerResponse.notFound().build());
169
}
170
}
171
```
172
173
### ServerRequest
174
175
Represents a server-side HTTP request in the functional web framework, providing access to method, URI, headers, and body.
176
177
```java { .api }
178
/**
179
* Represents a server-side HTTP request, as handled by a HandlerFunction.
180
*/
181
public interface ServerRequest {
182
183
/** Return the HTTP method */
184
HttpMethod method();
185
186
/** Return the HTTP method as a String */
187
String methodName();
188
189
/** Return the request URI */
190
URI uri();
191
192
/** Return the request path */
193
String path();
194
195
/** Return the query parameters */
196
MultiValueMap<String, String> queryParams();
197
198
/** Return a query parameter value */
199
default Optional<String> queryParam(String name);
200
201
/** Return the request headers */
202
ServerRequest.Headers headers();
203
204
/** Return the cookies */
205
MultiValueMap<String, HttpCookie> cookies();
206
207
/** Return the body as the given type */
208
<T> T body(Class<T> bodyType) throws IOException, ServletException;
209
210
/** Return the body as the given ParameterizedTypeReference */
211
<T> T body(ParameterizedTypeReference<T> bodyType) throws IOException, ServletException;
212
213
/** Return a path variable value */
214
default Optional<String> pathVariable(String name);
215
216
/** Return all path variables */
217
Map<String, String> pathVariables();
218
219
/** Return the web session */
220
WebSession session();
221
222
/** Return the principal */
223
default Optional<Principal> principal();
224
225
/** Return request attributes */
226
Map<String, Object> attributes();
227
228
/** Return an attribute value */
229
default Optional<Object> attribute(String name);
230
231
// Static factory methods
232
233
/** Create a ServerRequest based on the given HttpServletRequest */
234
static ServerRequest create(HttpServletRequest request, List<HttpMessageConverter<?>> messageReaders);
235
236
/**
237
* Nested interface for request headers.
238
*/
239
interface Headers {
240
/** Return header values for the given header name */
241
List<String> header(String headerName);
242
243
/** Return the first header value for the given name */
244
default Optional<String> firstHeader(String headerName);
245
246
/** Return the Accept header values */
247
List<MediaType> accept();
248
249
/** Return the Accept-Charset header values */
250
List<Charset> acceptCharset();
251
252
/** Return the Accept-Language header values */
253
List<Locale.LanguageRange> acceptLanguage();
254
255
/** Return the Content-Length header value */
256
OptionalLong contentLength();
257
258
/** Return the Content-Type header value */
259
Optional<MediaType> contentType();
260
261
/** Return all headers as HttpHeaders */
262
HttpHeaders asHttpHeaders();
263
}
264
}
265
```
266
267
**Usage Examples:**
268
269
```java
270
public ServerResponse handleRequest(ServerRequest request) {
271
// Access HTTP method and path
272
HttpMethod method = request.method();
273
String path = request.path();
274
275
// Access query parameters
276
Optional<String> limit = request.queryParam("limit");
277
Optional<String> offset = request.queryParam("offset");
278
279
// Access path variables
280
Optional<String> id = request.pathVariable("id");
281
Map<String, String> pathVars = request.pathVariables();
282
283
// Access headers
284
Optional<String> authHeader = request.headers().firstHeader("Authorization");
285
List<MediaType> acceptTypes = request.headers().accept();
286
287
// Access request body
288
try {
289
User user = request.body(User.class);
290
// Process user
291
} catch (Exception e) {
292
return ServerResponse.badRequest().build();
293
}
294
295
return ServerResponse.ok().build();
296
}
297
```
298
299
### ServerResponse
300
301
Represents a server-side HTTP response in the functional web framework, providing a fluent API for building responses.
302
303
```java { .api }
304
/**
305
* Represents a server-side HTTP response, as returned by a HandlerFunction.
306
*/
307
public interface ServerResponse {
308
309
/** Return the status code of this response */
310
HttpStatusCode statusCode();
311
312
/** Return the headers of this response */
313
HttpHeaders headers();
314
315
/**
316
* Write this response to the given HttpServletResponse.
317
* @param request the current request
318
* @param response the servlet response to write to
319
* @param context the context used for writing
320
* @return a ModelAndView, or null
321
*/
322
ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response, Context context)
323
throws ServletException, IOException;
324
325
// Static factory methods for different response types
326
327
/** Create a response with the given status */
328
static BodyBuilder status(HttpStatusCode status);
329
330
/** Create a response with the given status code */
331
static BodyBuilder status(int status);
332
333
/** Create a 200 OK response */
334
static BodyBuilder ok();
335
336
/** Create a 201 Created response with the given location */
337
static BodyBuilder created(URI location);
338
339
/** Create a 202 Accepted response */
340
static BodyBuilder accepted();
341
342
/** Create a 204 No Content response */
343
static HeadersBuilder<?> noContent();
344
345
/** Create a 400 Bad Request response */
346
static BodyBuilder badRequest();
347
348
/** Create a 404 Not Found response */
349
static HeadersBuilder<?> notFound();
350
351
/** Create a 422 Unprocessable Entity response */
352
static BodyBuilder unprocessableEntity();
353
354
/**
355
* Defines a builder for response headers.
356
*/
357
interface HeadersBuilder<B extends HeadersBuilder<B>> {
358
/** Add the given header value(s) under the given name */
359
B header(String headerName, String... headerValues);
360
361
/** Set the given header values under the given name */
362
B headers(HttpHeaders headers);
363
364
/** Add the given cookie */
365
B cookie(ResponseCookie cookie);
366
367
/** Set the given cookies */
368
B cookies(Consumer<MultiValueMap<String, ResponseCookie>> cookiesConsumer);
369
370
/** Allow the given origins for CORS */
371
B allow(HttpMethod... allowedMethods);
372
373
/** Set the ETag header */
374
B eTag(String etag);
375
376
/** Set the Last-Modified header */
377
B lastModified(ZonedDateTime lastModified);
378
379
/** Set the Location header */
380
B location(URI location);
381
382
/** Set the Cache-Control header */
383
B cacheControl(CacheControl cacheControl);
384
385
/** Set the Vary header */
386
B varyBy(String... requestHeaders);
387
388
/** Build the response */
389
ServerResponse build();
390
}
391
392
/**
393
* Defines a builder for responses with body.
394
*/
395
interface BodyBuilder extends HeadersBuilder<BodyBuilder> {
396
/** Set the Content-Length header */
397
BodyBuilder contentLength(long contentLength);
398
399
/** Set the Content-Type header */
400
BodyBuilder contentType(MediaType contentType);
401
402
/** Set the response body */
403
ServerResponse body(Object body);
404
405
/** Set the response body with the given type hint */
406
<T> ServerResponse body(T body, ParameterizedTypeReference<T> bodyType);
407
408
/** Render the given template with the model */
409
ServerResponse render(String name, Object... modelAttributes);
410
411
/** Render the given template with the model map */
412
ServerResponse render(String name, Map<String, ?> model);
413
}
414
415
/**
416
* Context used for writing the response.
417
*/
418
interface Context {
419
/** Return the message converters */
420
List<HttpMessageConverter<?>> messageConverters();
421
}
422
}
423
```
424
425
**Usage Examples:**
426
427
```java
428
public class ResponseExamples {
429
430
public ServerResponse jsonResponse(List<User> users) {
431
return ServerResponse.ok()
432
.contentType(MediaType.APPLICATION_JSON)
433
.body(users);
434
}
435
436
public ServerResponse createdResponse(User user) {
437
URI location = URI.create("/users/" + user.getId());
438
return ServerResponse.created(location)
439
.contentType(MediaType.APPLICATION_JSON)
440
.body(user);
441
}
442
443
public ServerResponse errorResponse(String message) {
444
Map<String, String> error = Map.of("error", message);
445
return ServerResponse.badRequest()
446
.contentType(MediaType.APPLICATION_JSON)
447
.body(error);
448
}
449
450
public ServerResponse fileResponse(Resource file) {
451
return ServerResponse.ok()
452
.contentType(MediaType.APPLICATION_OCTET_STREAM)
453
.header("Content-Disposition", "attachment; filename=\"" + file.getFilename() + "\"")
454
.body(file);
455
}
456
457
public ServerResponse templateResponse(String templateName, Map<String, Object> model) {
458
return ServerResponse.ok()
459
.render(templateName, model);
460
}
461
462
public ServerResponse redirectResponse(String url) {
463
return ServerResponse.status(HttpStatus.FOUND)
464
.location(URI.create(url))
465
.build();
466
}
467
}
468
```
469
470
### RequestPredicate
471
472
Functional interface representing a predicate (boolean-valued function) that tests server requests for routing decisions.
473
474
```java { .api }
475
/**
476
* Represents a predicate (boolean-valued function) that tests whether a request matches.
477
*/
478
@FunctionalInterface
479
public interface RequestPredicate {
480
481
/**
482
* Test this predicate against the given request.
483
* @param request the request to test against
484
* @return true if the request matches the predicate
485
*/
486
boolean test(ServerRequest request);
487
488
/** Return a composed predicate that represents logical AND */
489
default RequestPredicate and(RequestPredicate other);
490
491
/** Return a composed predicate that represents logical OR */
492
default RequestPredicate or(RequestPredicate other);
493
494
/** Return a predicate that represents logical NOT */
495
default RequestPredicate negate();
496
497
/**
498
* Optional method to return additional path variables from the predicate.
499
* Used by path pattern matching predicates.
500
*/
501
default Optional<ServerRequest> nest(ServerRequest request);
502
503
/**
504
* Accept the given visitor, calling the method that corresponds to this predicate.
505
*/
506
default void accept(Visitor visitor);
507
508
/**
509
* Visitor interface for RequestPredicate implementations.
510
*/
511
interface Visitor {
512
void method(Set<HttpMethod> methods);
513
void path(String pattern);
514
void pathExtension(String extension);
515
void header(String name, String value);
516
void queryParam(String name, String value);
517
void startNested();
518
void endNested();
519
void and();
520
void or();
521
void negate();
522
void unknown(RequestPredicate predicate);
523
}
524
}
525
```
526
527
## Utility Classes
528
529
### RouterFunctions
530
531
Central utility class providing static methods for creating and composing router functions.
532
533
```java { .api }
534
/**
535
* Central repository of utility methods that work with router functions.
536
*/
537
public abstract class RouterFunctions {
538
539
/** Create a route that matches the given predicate */
540
public static <T extends ServerResponse> RouterFunction<T> route(
541
RequestPredicate predicate, HandlerFunction<T> handlerFunction);
542
543
/** Create a nested route with the given predicate and router function */
544
public static <T extends ServerResponse> RouterFunction<T> nest(
545
RequestPredicate predicate, RouterFunction<T> routerFunction);
546
547
/** Create a router function for serving static resources */
548
public static RouterFunction<ServerResponse> resources(String pattern, Resource location);
549
550
/** Create a router function for serving resources with custom lookup */
551
public static RouterFunction<ServerResponse> resources(Function<ServerRequest, Optional<Resource>> lookupFunction);
552
553
/** Convert a RouterFunction to an HttpRequestHandler */
554
public static HttpRequestHandler toHttpHandler(RouterFunction<?> routerFunction);
555
556
/** Convert a RouterFunction to an HttpRequestHandler with MessageConverters */
557
public static HttpRequestHandler toHttpHandler(RouterFunction<?> routerFunction,
558
List<HttpMessageConverter<?>> messageConverters);
559
560
/** Convert a RouterFunction to a WebHandler */
561
public static WebHandler toWebHandler(RouterFunction<?> routerFunction);
562
}
563
```
564
565
**Usage Examples:**
566
567
```java
568
@Configuration
569
public class RouterConfiguration {
570
571
@Bean
572
public RouterFunction<ServerResponse> apiRoutes(UserHandler userHandler, ProductHandler productHandler) {
573
return nest(path("/api"),
574
nest(path("/users"),
575
route(GET(""), userHandler::findAll)
576
.andRoute(GET("/{id}"), userHandler::findById)
577
.andRoute(POST(""), userHandler::create)
578
.andRoute(PUT("/{id}"), userHandler::update)
579
.andRoute(DELETE("/{id}"), userHandler::delete)
580
).andNest(path("/products"),
581
route(GET(""), productHandler::findAll)
582
.andRoute(GET("/{id}"), productHandler::findById)
583
.andRoute(POST(""), productHandler::create)
584
)
585
);
586
}
587
588
@Bean
589
public RouterFunction<ServerResponse> staticResources() {
590
return resources("/static/**", new ClassPathResource("static/"))
591
.andRoute(resources("/uploads/**", new FileSystemResource("/var/uploads/")));
592
}
593
594
@Bean
595
public HttpRequestHandler httpHandler(RouterFunction<ServerResponse> routes) {
596
return RouterFunctions.toHttpHandler(routes);
597
}
598
}
599
```
600
601
### RequestPredicates
602
603
Utility class providing static methods for creating common request predicates used in routing.
604
605
```java { .api }
606
/**
607
* Implementations of RequestPredicate that implement various useful request matching operations.
608
*/
609
public abstract class RequestPredicates {
610
611
/** Match all requests */
612
public static RequestPredicate all();
613
614
/** Match requests with the given HTTP method */
615
public static RequestPredicate method(HttpMethod httpMethod);
616
617
/** Match requests with any of the given HTTP methods */
618
public static RequestPredicate methods(HttpMethod... httpMethods);
619
620
/** Match requests with the given path pattern */
621
public static RequestPredicate path(String pattern);
622
623
/** Match requests with the given path extension */
624
public static RequestPredicate pathExtension(String extension);
625
626
/** Match requests with the given path extension predicate */
627
public static RequestPredicate pathExtension(Predicate<String> extensionPredicate);
628
629
/** Match requests that accept the given media types */
630
public static RequestPredicate accept(MediaType... mediaTypes);
631
632
/** Match requests with the given content type */
633
public static RequestPredicate contentType(MediaType... mediaTypes);
634
635
/** Match requests with headers matching the given predicate */
636
public static RequestPredicate headers(Predicate<ServerRequest.Headers> headersPredicate);
637
638
/** Match requests with the given header name and value */
639
public static RequestPredicate header(String name, String value);
640
641
/** Match requests with query parameter matching the given predicate */
642
public static RequestPredicate queryParam(String name, Predicate<String> predicate);
643
644
/** Match requests with the given query parameter value */
645
public static RequestPredicate queryParam(String name, String value);
646
647
// HTTP method convenience methods
648
649
/** Match GET requests */
650
public static RequestPredicate GET(String pattern);
651
652
/** Match HEAD requests */
653
public static RequestPredicate HEAD(String pattern);
654
655
/** Match POST requests */
656
public static RequestPredicate POST(String pattern);
657
658
/** Match PUT requests */
659
public static RequestPredicate PUT(String pattern);
660
661
/** Match PATCH requests */
662
public static RequestPredicate PATCH(String pattern);
663
664
/** Match DELETE requests */
665
public static RequestPredicate DELETE(String pattern);
666
667
/** Match OPTIONS requests */
668
public static RequestPredicate OPTIONS(String pattern);
669
}
670
```
671
672
**Usage Examples:**
673
674
```java
675
public class PredicateExamples {
676
677
public RouterFunction<ServerResponse> complexRouting() {
678
return route(GET("/users").and(accept(MediaType.APPLICATION_JSON)), this::getUsersJson)
679
.andRoute(GET("/users").and(accept(MediaType.APPLICATION_XML)), this::getUsersXml)
680
.andRoute(POST("/users").and(contentType(MediaType.APPLICATION_JSON)), this::createUser)
681
.andRoute(path("/admin/**").and(header("Authorization", "Bearer.*")), this::adminHandler)
682
.andRoute(queryParam("version", version -> version.equals("2")), this::versionTwoHandler)
683
.andRoute(pathExtension("json"), this::jsonHandler)
684
.andRoute(pathExtension(ext -> Arrays.asList("jpg", "png", "gif").contains(ext)), this::imageHandler);
685
}
686
687
public RouterFunction<ServerResponse> conditionalRouting() {
688
RequestPredicate isApiRequest = path("/api/**");
689
RequestPredicate isAdminRequest = path("/admin/**");
690
RequestPredicate hasApiKey = header("X-API-Key", key -> !key.isEmpty());
691
RequestPredicate hasAdminRole = header("X-User-Role", "admin");
692
693
return route(isApiRequest.and(hasApiKey), this::handleApiRequest)
694
.andRoute(isAdminRequest.and(hasAdminRole), this::handleAdminRequest)
695
.andRoute(all(), this::handlePublicRequest);
696
}
697
}
698
```
699
700
## Filter Functions
701
702
### HandlerFilterFunction
703
704
Functional interface for filtering handler functions, allowing for cross-cutting concerns like authentication, logging, and error handling.
705
706
```java { .api }
707
/**
708
* Represents a filter function for handler functions that can pre- and post-process requests.
709
* @param <T> the type of response handled by the handler function being filtered
710
* @param <R> the type of response returned by the filter
711
*/
712
@FunctionalInterface
713
public interface HandlerFilterFunction<T extends ServerResponse, R extends ServerResponse> {
714
715
/**
716
* Apply this filter to the given handler function.
717
* @param request the request
718
* @param next the next handler or filter in the chain
719
* @return the filtered response
720
*/
721
R filter(ServerRequest request, HandlerFunction<T> next) throws Exception;
722
723
/** Return a composed filter that first applies this filter, then the after filter */
724
default HandlerFilterFunction<T, R> andThen(HandlerFilterFunction<R, R> after);
725
}
726
```
727
728
**Usage Examples:**
729
730
```java
731
public class FilterExamples {
732
733
public HandlerFilterFunction<ServerResponse, ServerResponse> loggingFilter() {
734
return (request, next) -> {
735
long startTime = System.currentTimeMillis();
736
log.info("Request: {} {}", request.method(), request.path());
737
738
ServerResponse response = next.handle(request);
739
740
long duration = System.currentTimeMillis() - startTime;
741
log.info("Response: {} - {}ms", response.statusCode(), duration);
742
743
return response;
744
};
745
}
746
747
public HandlerFilterFunction<ServerResponse, ServerResponse> authenticationFilter() {
748
return (request, next) -> {
749
Optional<String> authHeader = request.headers().firstHeader("Authorization");
750
751
if (authHeader.isEmpty() || !authHeader.get().startsWith("Bearer ")) {
752
return ServerResponse.status(HttpStatus.UNAUTHORIZED).build();
753
}
754
755
String token = authHeader.get().substring(7);
756
if (!tokenService.isValid(token)) {
757
return ServerResponse.status(HttpStatus.UNAUTHORIZED).build();
758
}
759
760
return next.handle(request);
761
};
762
}
763
764
public HandlerFilterFunction<ServerResponse, ServerResponse> errorHandlingFilter() {
765
return (request, next) -> {
766
try {
767
return next.handle(request);
768
} catch (ValidationException e) {
769
return ServerResponse.badRequest()
770
.contentType(MediaType.APPLICATION_JSON)
771
.body(Map.of("error", e.getMessage()));
772
} catch (NotFoundException e) {
773
return ServerResponse.notFound().build();
774
} catch (Exception e) {
775
log.error("Unexpected error processing request", e);
776
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
777
.body(Map.of("error", "Internal server error"));
778
}
779
};
780
}
781
782
@Bean
783
public RouterFunction<ServerResponse> filteredRoutes() {
784
return route(GET("/api/users"), userHandler::getUsers)
785
.andRoute(POST("/api/users"), userHandler::createUser)
786
.filter(loggingFilter())
787
.filter(authenticationFilter())
788
.filter(errorHandlingFilter());
789
}
790
}
791
```