Aspectran provides a standardized response model to maintain consistency in RESTful web services and to allow developers to intuitively and precisely control HTTP status codes, headers, and data transformation. This guide covers all aspects of designing APIs that comply with Aspectran’s standard specifications using RestResponse and its implementations.
1. Core Concepts
REST response handling in Aspectran goes beyond simply returning raw data; it aims to encapsulate the success status (success), data payload (data), and error information (error) into a single standardized format for predictable client communication.
1.1 Response Container: ResponsePayload
Every standard REST response (SuccessResponse, FailureResponse) is internally wrapped in a com.aspectran.web.support.rest.response.ResponsePayload object. This object has a consistent structure so that clients can process responses predictably:
- success: A boolean (
boolean) indicating whether the operation was successful. (Always present) - data: The actual data object (
Object) to be delivered to the client as a result of the operation.- Successful responses (
success: true): Included in serialization whendatais non-null. Whennull, thedatafield is omitted, returning simply{"success": true}. - Failed responses (
success: false): Only included in special cases where data is required (e.g., partial success results or debugging metadata).
- Successful responses (
- error: An
ErrorPayloadobject containing detailed error information when an operation fails.code: A unique code identifying the error (String, required).message: A detailed descriptive error message (String, optional).
The ResponsePayload.toEntity() method constructs a conditional entity map to avoid unnecessary null serialization and produce clean JSON/APON/XML structures:
| Response State | success | data Field | error Field | Resulting Payload Example (JSON) |
|---|---|---|---|---|
| Success (with data) | true | Included (Object) | Omitted | {"success": true, "data": {...}} |
| Success (without data) | true | Omitted | Omitted | {"success": true} |
| Failure (error code/message) | false | Omitted (default) | Included (ErrorPayload) | {"success": false, "error": {"code": "...", "message": "..."}} |
| Failure (data + error) | false | Included (Object) | Included (ErrorPayload) | {"success": false, "data": {...}, "error": {...}} |
1.2 Class Hierarchy
Aspectran’s REST response architecture is designed with the following class hierarchy for flexible extensibility and consistent transformation logic:
com.aspectran.core.activity.response.transform.CustomTransformer (Interface)
└─ com.aspectran.web.activity.response.RestResponse (Interface)
└─ com.aspectran.web.activity.response.AbstractRestResponse (Abstract Class)
└─ com.aspectran.web.activity.response.DefaultRestResponse (Class)
├─ com.aspectran.web.support.rest.response.SuccessResponse (Class)
└─ com.aspectran.web.support.rest.response.FailureResponse (Class)
- RestResponse: The core interface defining the fluent API specification for manipulating RESTful HTTP responses. It extends
CustomTransformerto seamlessly integrate with Aspectran’s self-transformation mechanism. - AbstractRestResponse: An abstract class providing common implementations for status codes, header maps, media types, and the intelligent content negotiation algorithm (
determineAcceptContentType). - DefaultRestResponse: The default implementation with built-in serialization logic for JSON, APON, XML, Plain Text, and HTML formats. When given a
ResponsePayload, it callstoEntity()to automatically convert it into the standard payload structure. - SuccessResponse: Used for returning successful results with 2xx status codes. It internally instantiates
ResponsePayload(true, data), defaulting to HTTP status200 OK. - FailureResponse: Used for returning error causes with 4xx or 5xx status codes. It internally instantiates
ResponsePayload(false)and anErrorPayload, providing fluent chaining methods to set error codes and messages.
2. How to Apply
There are two primary ways to deliver a response to a client using RestResponse in Aspectran.
2.1 Using as an Action Method Return Value (Recommended)
This is the most recommended approach, where the return type of the action method is declared as RestResponse (or SuccessResponse, FailureResponse).
Key Feature: Self-Transformation RestResponse implements the CustomTransformer interface. When an action’s result is of type CustomTransformer, the Aspectran engine allows the object itself to execute its own defined transformation logic (transform(Activity)), even without an explicit @Transform annotation or <transform> configuration in XML. This delegates the responsibility of determining the response format and serialization to the response object itself.
@Request("/api/data")
// No @Transform or @Dispatch configuration required!
public RestResponse getData() {
return new SuccessResponse(dataService.findAll());
}
2.2 Explicit Transformation via Translet
In environments where the method return type is void or where the response cannot be directly controlled via a return value (such as in an Aspect, Interceptor, or Advice), call the translet.transform(RestResponse) method. Calling this immediately triggers the transformation and response process using the provided response object.
@Before
public void checkAuth(Translet translet) {
if (!isAuthenticated()) {
// Immediately execute transformation process with a 403 Forbidden error response
translet.transform(new FailureResponse().forbidden().setError("FORBIDDEN", "Authentication required."));
}
}
3. Supported Content Types
Aspectran’s DefaultRestResponse automatically transforms data into various media types based on client requirements without requiring third-party library configuration.
| Media Type | Extension | Description |
|---|---|---|
application/json | .json | Standard JSON format (JsonWriter, JSONP callback support) |
application/apon | .apon | Aspectran Parameters Object Notation (APON, concise text-based format) |
application/xml | .xml | Standard XML format (XmlTransformResponse, root tag via name) |
text/plain | .txt | Plain text output of the data’s string representation |
text/html | .html, .htm | HTML string output |
Content Negotiation Priority Algorithm
AbstractRestResponse.determineAcceptContentType(Activity) resolves the optimal media type using a 4-step precedence order:
- URL Path Extension (
favorPathExtension):- If the request path ends with an extension like
.json,.xml,.apon,.txt,.html, or.htm, that media type is selected first. (Default:true) - If an unknown extension is requested and
ignoreUnknownPathExtensionsisfalse, a406 Not Acceptableerror is thrown.
- If the request path ends with an extension like
- HTTP Accept Header (
ignoreAcceptHeader):- If no extension is present or no type was resolved by extension, the client’s
Acceptheader quality values are analyzed and matched against the server’s supported media types (supportedContentTypes).
- If no extension is present or no type was resolved by extension, the client’s
- Default Media Type (
defaultContentType):- If the
Acceptheader is*/*or no matching media type is found, the instance’sdefaultContentTypeor the Translet setting keyresponse.defaultContentType(RestResponse.RESPONSE_DEFAULT_CONTENT_TYPE) is applied.
- If the
- 406 Not Acceptable:
- If no compatible media type can be resolved through any of the above steps, HTTP status code
406 Not Acceptableis returned.
- If no compatible media type can be resolved through any of the above steps, HTTP status code
4. HTTP Status Code Control (Fluent API)
AbstractRestResponse provides various methods to intuitively configure HTTP status codes. These methods support Method Chaining, resulting in concise and readable code.
4.1 Success Responses (2xx Success)
ok(): 200 OK. Indicates the request succeeded. (Default)created(): 201 Created. Indicates a new resource was created.created(String location): 201 Created. Automatically adds the URI of the created resource to theLocationresponse header.accepted(): 202 Accepted. Indicates the request has been accepted for processing, but processing has not completed.noContent(): 204 No Content. Indicates success, but there is no entity body to return.
4.2 Redirection (3xx Redirection)
movedPermanently(): 301 Moved Permanently. Resource URI has changed permanently.seeOther(): 303 See Other. Directs client to get the resource at another URI.notModified(): 304 Not Modified. Cached resource has not been modified.temporaryRedirect(): 307 Temporary Redirect. Temporary redirection to another URI.
4.3 Client Errors (4xx Client Errors)
badRequest(): 400 Bad Request. Malformed request syntax or invalid parameters.unauthorized(): 401 Unauthorized. Missing or invalid authentication credentials.forbidden(): 403 Forbidden. Server understood request but refuses authorization (e.g., insufficient permissions).notFound(): 404 Not Found. Requested resource could not be found.methodNotAllowed(): 405 Method Not Allowed. HTTP method is not supported for the requested resource.notAcceptable(): 406 Not Acceptable. Cannot generate response content matching the Accept header.conflict(): 409 Conflict. Request conflicts with current state of the target resource.preconditionFailed(): 412 Precondition Failed. Target resource precondition failed.unsupportedMediaType(): 415 Unsupported Media Type. Payload media type is not supported.
4.4 Server Errors (5xx Server Errors)
internalServerError(): 500 Internal Server Error. Unexpected server-side error encountered.
4.5 General Status Code Methods
setStatus(int status): Sets the integer HTTP status code directly.setStatus(HttpStatus status): Sets the status usingcom.aspectran.web.support.http.HttpStatusenum constant.getStatus(): Returns the currently configured integer HTTP status code.
5. Data, Header, and Serialization Methods
In addition to status codes, methods are provided to finely control the response data and serialization behavior:
setData(Object data): Explicitly sets the object for thedatafield in the payload.setData(String name, Object data): Assigns a root key name to the data, which is useful for naming the root XML element or wrapping root JSON objects.setHeader(String name, String value): Sets an HTTP response header (overwrites any existing value).addHeader(String name, String value): Adds an HTTP response header value (supports multiple values for the same header name).prettyPrint(boolean prettyPrint): Determines whether to format output data (JSON, APON, XML) with indentation and line breaks.nullWritable(boolean nullWritable): Determines whether to include fields withnullvalues during serialization. (Default:false)stringifyContext(StringifyContext stringifyContext): Explicitly specifies advanced serialization configuration viaStringifyContext.defaultContentType(MediaType defaultContentType): Dynamically overrides the default media type for the instance.favorPathExtension(boolean)/ignoreAcceptHeader(boolean)/ignoreUnknownPathExtensions(boolean): Overrides content negotiation settings per instance.
6. Practical Examples
Practical examples demonstrating Aspectran’s ${...} path variable mapping and standard REST response classes.
Example 1: Basic Success Response (200 OK)
Returning a retrieved entity or DTO encapsulated in a standard response.
@Request("/api/user/${userId}")
public RestResponse getUser(String userId) {
User user = userService.getUserById(userId);
if (user == null) {
return new FailureResponse()
.notFound()
.setError("USER_NOT_FOUND", "User not found: " + userId);
}
// Return data encapsulated in standard ResponsePayload with a 200 OK status code
return new SuccessResponse(user);
}
Response Data Sample (JSON):
{
"success": true,
"data": {
"userId": "user-01",
"username": "aspectran",
"email": "[email protected]"
}
}
Response Data Sample (APON - Requested via /api/user/user-01.apon):
success: true
data: {
userId: user-01
username: aspectran
email: [email protected]
}
Response Data Sample (XML - Requested via /api/user/user-01.xml):
<?xml version="1.0" encoding="UTF-8"?>
<response>
<success>true</success>
<data>
<userId>user-01</userId>
<username>aspectran</username>
<email>[email protected]</email>
</data>
</response>
Example 2: Resource Creation and Location Header (201 Created)
Informing the client of the location URI of a newly created resource.
@RequestToPost("/api/orders")
public RestResponse createOrder(Order order) {
orderService.placeOrder(order);
// Set 201 Created status code and automatically add the Location header
return new SuccessResponse(order)
.created("/api/orders/" + order.getId());
}
Response Header and Body Sample (HTTP):
HTTP/1.1 201 Created
Location: /api/orders/ORD-1004
Content-Type: application/json;charset=UTF-8
{
"success": true,
"data": {
"orderId": "ORD-1004",
"item": "Aspectran Pro License",
"status": "COMPLETED"
}
}
Example 3: Validation Failure and Error Handling (400 Bad Request)
Returning a detailed error code and user-friendly message when input validation fails.
@RequestToPost("/api/register")
public RestResponse register(User user) {
if (StringUtils.isEmpty(user.getEmail())) {
return new FailureResponse()
.badRequest() // Set HTTP 400 status code
.setError("REQUIRED_FIELD", "Email is a required field.");
}
userService.register(user);
return new SuccessResponse("Registered successfully");
}
Response Data Sample (JSON - Validation Failure):
{
"success": false,
"error": {
"code": "REQUIRED_FIELD",
"message": "Email is a required field."
}
}
Example 4: Authentication and Access Control (401 Unauthorized, 403 Forbidden)
Controlling secure access with clear status codes and reasons for denial.
@Request("/api/admin/settings")
public RestResponse getAdminSettings(UserInfo userInfo) {
if (userInfo == null) {
// User not logged in (401 Unauthorized)
return new FailureResponse()
.unauthorized()
.setError("UNAUTHORIZED", "Authentication is required.");
}
if (!userInfo.hasRole("ADMIN")) {
// User lacks administrative privileges (403 Forbidden)
return new FailureResponse()
.forbidden()
.setError("ACCESS_DENIED", "Admin privileges are required.");
}
return new SuccessResponse(configService.getAdminConfig());
}
Response Data Sample (JSON - Insufficient Privileges):
{
"success": false,
"error": {
"code": "ACCESS_DENIED",
"message": "Admin privileges are required."
}
}
Example 5: Formatting and Custom Header Control
Formatting response data for readability and setting cache-control alongside custom metadata headers.
@Request("/api/system/stats")
public RestResponse getSystemStats() {
Map<String, Object> stats = statsService.getGlobalStats();
return new SuccessResponse(stats)
.prettyPrint(true) // Enable indentation and line breaks for readability
.nullWritable(false) // Omit null fields
.setHeader("Cache-Control", "no-cache, no-store, must-revalidate")
.addHeader("X-Node-Id", "node-01")
.addHeader("X-Response-Time", String.valueOf(System.currentTimeMillis()));
}
Example 6: Backend API Relay (Using RestRequest)
Using RestRequest to invoke external microservices or internal cluster node APIs and relaying the result directly as the current server’s response. RestRequest.retrieve() returns either a SuccessResponse or FailureResponse based on the remote status code, providing seamless client-server symmetry.
@Request("/relay/weather")
public RestResponse relayWeatherRequest(CloseableHttpClient httpClient) {
String targetUrl = "https://internal-api.example.com/weather/today";
try {
RestRequest restRequest = new RestRequest(httpClient);
// The result of RestRequest.retrieve() is already a SuccessResponse or FailureResponse
RestResponse response = restRequest.get()
.url(targetUrl)
.retrieve();
// Return the received standard response directly as the Translet response
return response;
} catch (IOException e) {
return new FailureResponse()
.internalServerError()
.setError("RELAY_ERROR", "Failed to communicate with remote server: " + e.getMessage());
}
}
Example 7: Global Exception Handling via Aspect
Intercepting exceptions thrown across the application and transforming them into standard FailureResponse objects ensures predictable and consistent error structures for API consumers.
@Component
@Aspect
public class GlobalRestExceptionAdvice {
private static final Logger logger = LoggerFactory.getLogger(GlobalRestExceptionAdvice.class);
@AfterThrown(target = "activity", thrown = "java.lang.IllegalArgumentException")
public void handleIllegalArgument(Translet translet, Exception ex) {
logger.warn("Invalid client argument: {}", ex.getMessage());
RestResponse response = new FailureResponse()
.badRequest()
.setError("INVALID_PARAMETER", ex.getMessage());
translet.transform(response);
}
@AfterThrown(target = "activity", thrown = "java.lang.Exception")
public void handleGeneralException(Translet translet, Exception ex) {
logger.error("Unhandled server exception occurred", ex);
RestResponse response = new FailureResponse()
.internalServerError()
.setError("INTERNAL_SERVER_ERROR", "An internal server error occurred.");
translet.transform(response);
}
}
7. Best Practices
- Use Standard Classes: Avoid manually constructing response bodies using
Map.of("success", true, "data", ...)or defining custom DTOs forsuccessfields. Always useSuccessResponseandFailureResponseto strictly follow theResponsePayloadstandard specification. - Clean Empty Success Responses: When no data payload is needed (e.g., delete operations or state toggles), return
new SuccessResponse()ornew SuccessResponse().noContent()to minimize payload overhead. - Establish a Project-wide Error Code Convention: Manage error codes (
ErrorPayload.code) as centralized constants (enums or static constants) across the project for clear client-side error handling. - Align Frontend Communication Contracts: Web frontends and API consumers should inspect the top-level
res.successboolean field (if (res.success) { ... }) to branch execution, and accessres.error.codeandres.error.messageupon failure. - Leverage Content Negotiation: When clients append
.json,.apon, or.xmlextensions, Aspectran automatically performs format serialization, allowing a single action method to serve multi-format APIs effortlessly.