Presentation Layer Implementation Rules
The presentation layer takes the seam between HTTP and the application. Mixing business decisions into this layer means none of it can be reused the day you want to call the same processing from a batch job.
Four kinds of classes live here, along with the exceptions that decide the status code.
| Package | Class | Role |
|---|---|---|
presentation.endpoint | {Resource}Endpoint, {Resource}EndpointFactory | The entry point for the REST API |
BusinessErrorException | Exception that answers a request that does not hold up as business with a 422 | |
presentation.request | {Operation}Request | Where the request lands |
presentation.response | {Operation}Response | Assembling the response |
presentation.validator | {Operation}Validator | Validating the shape of the input |
ValidationException | Exception that answers malformed input with a 400 |
The Structure of an Endpoint
REST APIs are published with Web API Maker. What that takes is two classes: a factory and an endpoint.
package jp.co.example.foo.presentation.endpoint;
import jp.co.intra_mart.foundation.web_api_maker.annotation.ProvideFactory;
import jp.co.intra_mart.foundation.web_api_maker.annotation.ProvideService;
import jp.co.intra_mart.foundation.web_api_maker.annotation.WebAPIMaker;
/**
* Factory class for the order endpoint.
*/
@WebAPIMaker
public class OrderEndpointFactory {
@ProvideFactory
public static OrderEndpointFactory getFactory() {
return new OrderEndpointFactory();
}
@ProvideService
public OrderEndpoint getEndpoint() {
return new OrderEndpoint();
}
}
The endpoint carries only the path, the method, the authentication settings, input validation, the call into the use case, and the replacement of exceptions.
package jp.co.example.foo.presentation.endpoint;
import java.util.List;
import jp.co.example.foo.application.exception.OrderAppException;
import jp.co.example.foo.application.usecase.GetOrderUseCase;
import jp.co.example.foo.presentation.response.OrderResponse;
import jp.co.example.foo.presentation.validator.GetOrderValidator;
import jp.co.example.foo.presentation.validator.ValidationException;
import jp.co.intra_mart.foundation.authz.annotation.Authz;
import jp.co.intra_mart.foundation.web_api_maker.annotation.GET;
import jp.co.intra_mart.foundation.web_api_maker.annotation.IMAuthentication;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Path;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Required;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Variable;
/**
* REST API endpoint that provides order information.
*/
@IMAuthentication
@Authz(uri = "service://foo/web/tenant", action = "execute")
public class OrderEndpoint {
private final GetOrderUseCase useCase = new GetOrderUseCase();
@Path("/api/foo/order/{orderId}")
@GET(summary = "発注取得", description = "発注IDを指定して発注情報を取得します。")
public OrderResponse get(
@Required @Variable(name = "orderId", description = "発注ID") final String orderId)
throws BusinessErrorException {
final List<String> errors = GetOrderValidator.validate(orderId);
if (!errors.isEmpty()) {
throw new ValidationException(errors);
}
try {
return useCase.execute(orderId);
} catch (final OrderAppException e) {
throw new BusinessErrorException(e.getMessage(), e);
}
}
}
The endpoint class, and the classes used for responses and requests, are all public.
Registering the Package
The endpoint's package is written, one per line, in src/main/resources/META-INF/im_web_api_maker/packages.
jp.co.example.foo.presentation.endpoint
What goes there is the package holding the endpoint classes, not the factory class. Forgetting to register it means the API is not recognized even when the annotations are correct, and every access returns 404. When endpoints are spread across several packages, add one line per package.
Authentication and Authorization
Authentication and authorization are declared with annotations.
@IMAuthentication requires the caller to be logged in, and @Authz requires a permission on an authorization resource.
A class takes only one authentication annotation (@IMAuthentication, @BasicAuthentication, or @OAuth).
The authorization resource given in the uri of @Authz must be registered in IM-Authz beforehand.
Adding @Authz alone does not make authorization work.
The responses when authentication or authorization rejects a request are as follows (for @IMAuthentication).
| Situation | Status | Response |
|---|---|---|
| Not logged in | 404 | An HTML error page. Never a 401 |
| Logged in but without permission | 403 | An object with error and data |
The resource in the uri of @Authz is not registered | 500 | An HTML error page (ResourceNotFoundException) |
An access without a login becomes a 404 because the authentication check runs before the endpoint is called. The shape of the response is covered in Response Format.
Registering authorization resources, and combining @BasicAuthentication or @OAuth, belong to Web API Maker and the authorization feature themselves and are outside the scope of this guide.
Secure Tokens
@Secured verifies the secure token and protects against CSRF (cross-site request forgery).
Its role differs from that of an authentication annotation, which decides who the access is made as.
APIs that change state and are called from a browser (POST, PUT, DELETE) take both.
A read-only GET does not take @Secured.
A client calling an API with @Secured sends the token in the X-Intramart-Secure-Token request header.
When calling from a JSSP screen, the token taken from <meta name="im_secure_token"> can be used as it is.
A request without a token gets a 403.
APIs for external systems that use @BasicAuthentication or @OAuth normally do not take @Secured.
What It Does and Does Not Do
An endpoint does nothing but pass the parameters it receives on to a use case.
It is responsible for four things.
- Receiving and parsing the request
- Validating the shape of the input
- Calling the use case
- Returning the response and deciding the status code
It is responsible for none of these four.
- Deciding business rules
- Manipulating domain models
- Accessing the database
- Controlling transactions
If SessionTemplate or DAOFactory shows up in this layer, a boundary has been crossed.
Request and Response DTOs
Data exchanged with the outside is carried in dedicated classes.
package jp.co.example.foo.presentation.response;
import java.math.BigDecimal;
import jp.co.example.foo.domain.model.Order;
/**
* Response of the order retrieval API.
*/
public class OrderResponse {
private String orderId;
private String customerName;
private BigDecimal amount;
/**
* Builds a response from a domain model.
* @param order the order
* @return the response
*/
public static OrderResponse fromDomainModel(final Order order) {
final OrderResponse response = new OrderResponse();
response.orderId = order.getOrderId();
response.customerName = order.getCustomerName();
response.amount = order.getAmount();
return response;
}
// Getters and setters omitted
}
A DTO has a no-argument constructor and a getter and setter for each property.
Web API Maker reads and writes only properties that have both a getter and a setter.
A property whose value is null is not written to the response.
Keeping the conversion as a static method on the response means the domain model never has to know that the DTO exists.
Growing a toResponse() on the domain model instead makes the domain layer reference the presentation layer, which reverses the direction of the dependency.
Why an entity is not returned as it is was covered in Overall Architecture. Returning a domain model directly is avoided as well. A domain model is a class that carries business rules, and tying its shape to the compatibility of an API means it can no longer be changed for business reasons.
A DTO is needed on the request side when a POST or a PUT takes a request body.
Values arriving in the path or the query string can be bound straight to parameters with @Variable and @Parameter, so the get method above needs no DTO at all.
package jp.co.example.foo.presentation.request;
import java.math.BigDecimal;
/**
* Request of the order registration API.
*/
public class RegisterOrderRequest {
private String customerName;
private BigDecimal amount;
// Getters and setters omitted
}
An endpoint that takes a body receives the DTO through a parameter annotated with @Body.
Because the API changes state, it takes @Secured.
@Path("/api/foo/order")
@POST(summary = "発注登録", description = "発注情報を登録します。")
@Secured
public OrderResponse register(@Body final RegisterOrderRequest request) throws BusinessErrorException {
final List<String> errors = RegisterOrderValidator.validate(request);
if (!errors.isEmpty()) {
throw new ValidationException(errors);
}
try {
return registerUseCase.execute(request);
} catch (final OrderAppException e) {
throw new BusinessErrorException(e.getMessage(), e);
}
}
Where the response was assembled by a static method on OrderResponse, the conversion from a request DTO into a domain model is done by the use case.
Giving the request DTO a toDomainModel() would let a presentation-layer class assemble a domain model, which turns one entrance to the business rules into two.
Input Validation
Validation is not written inline in the endpoint; it is carved out into a Validator.
package jp.co.example.foo.presentation.validator;
import java.util.ArrayList;
import java.util.List;
/**
* Validates the input of the order retrieval API.
*/
public final class GetOrderValidator {
private GetOrderValidator() {
}
/**
* Validates an order ID.
* @param orderId the order ID
* @return the list of error messages, empty when there is no problem
*/
public static List<String> validate(final String orderId) {
final List<String> errors = new ArrayList<>();
if (orderId == null || orderId.isEmpty()) {
errors.add("orderId は必須です");
} else if (orderId.length() > 15) {
errors.add("orderId は15文字以内で指定してください");
}
return errors;
}
}
Rather than throwing as soon as an error is found, the errors accumulate in a list and are returned together. When several fields are wrong, a single response can report all of them.
If there is even one error, the endpoint throws a ValidationException.
package jp.co.example.foo.presentation.validator;
import java.util.List;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Response;
import jp.co.intra_mart.foundation.web_api_maker.annotation.ReturnValue;
/**
* Exception representing malformed input.
*/
@Response(code = 400)
public class ValidationException extends RuntimeException {
private final List<String> errors;
public ValidationException(final List<String> errors) {
super("入力内容に誤りがあります");
this.errors = errors;
}
/**
* Returns the list of errors found by validation.
* @return the list of error messages
*/
@ReturnValue
public List<String> getErrors() {
return errors;
}
}
@Response(code = 400) decides the status code, and the getter annotated with @ReturnValue decides what goes into the data of the response.
What is checked here is only the shape of the request. Whether the amount holds up as business is decided by a service in the domain layer. Services are also called from batch jobs that never pass through the presentation layer, so validation is needed in both places. Where the line falls is covered in Infrastructure Layer Implementation Rules.
Input rejected by validation is not logged.
It is a mistake by the user rather than a system failure, and recording it as ERROR buries the real failures.
Authorization errors are not logged for the same reason.
An authorization failure is handled by the authorization feature.
From Exceptions to HTTP
The status code is decided by the @Response(code = ...) on the class of the exception thrown from the endpoint.
An exception without @Response becomes a 500.
| Exception | Status | Meaning |
|---|---|---|
ValidationException | 400 | The request is malformed |
BusinessErrorException | 422 | Well formed, but does not hold up as business |
An exception without @Response | 500 | An unanticipated failure |
The application layer's {AppName}Exception does not take @Response.
Adding it would make the application layer depend on Web API Maker.
The endpoint catches it and replaces it with this layer's exception annotated with @Response(code = 422).
package jp.co.example.foo.presentation.endpoint;
import jp.co.intra_mart.foundation.web_api_maker.annotation.Response;
/**
* Exception representing a request that does not hold up as business.
*/
@Response(code = 422)
public class BusinessErrorException extends Exception {
public BusinessErrorException(final String message, final Throwable cause) {
super(message, cause);
}
}
400 and 422 are separated because the caller's course of action differs. A 400 may succeed once the request is fixed; a 422 will not succeed no matter how the request is adjusted.
To report that no matching data exists as a 404, throw an exception annotated with @Response(code = 404).
Logging differs by exception as well.
Business errors are recorded at WARN, and unanticipated failures at ERROR with the full stack trace.
Validation errors and authorization errors are not recorded.
Response Format
The return value of an endpoint does not become the response body as it is.
Web API Maker wraps it in an object with error and data before returning it.
This is the body on success (Accept: application/json).
{
"error": false,
"data": {
"orderId": "ORDER001",
"customerName": "株式会社サンプル",
"amount": 10000
}
}
This is the body when an exception occurs.
{
"error": true,
"errorMessage": "入力内容に誤りがあります",
"data": {
"errors": ["orderId は15文字以内で指定してください"]
}
}
| Property | Content |
|---|---|
error | Whether an exception occurred. false on success |
data | On success, the endpoint's return value. On an exception, the value of the getter annotated with @ReturnValue (omitted if there is none) |
errorMessage | The exception's message, output only when an exception occurs |
The client takes the business data out of data.
Referencing a property without unwrapping gives undefined, even when the API is working correctly.
Send requests with Accept: application/json.
Without it, there is no guarantee that the response comes back as JSON.
The response is wrapped when processing reaches the endpoint method.
- Wrapped: success, exceptions thrown from the endpoint (with or without
@Response), a missing@Requiredvalue (400), and rejection by@Securedor@Authz(403) - Not wrapped: access without a login (404), a URL that is not found (404), an invalid
Accept(406), and exceptions raised before the endpoint (500). These come back as HTML or plain text
The client therefore decides in this order.
- Parse the body as JSON. An exception annotated with
@Responsecomes back wrapped even with a status other than 200, so do not stop at the status code - If it parses and
errorisfalse, usedataas the business data - If
erroristrue, treaterrorMessageanddataas the error information - If parsing fails or there is no
error, treat it as an unanticipated error
Related Documentation
- Application Layer Implementation Rules: Implementing the use case that the endpoint calls
- An Implementation Across Every Layer: The full set from endpoint down to the database
- Overall Architecture: The responsibilities of each layer, the exception hierarchy, and the package structure
- Anti-Patterns: Typical cases such as calling a DAO straight from an endpoint