Skip to content

Commit fd44d02

Browse files
committed
Refactor: standardize API response structure across all controllers
- Add ApiResponse<T> wrapper class with success/message/data/meta fields - Add ResponseFactory for standardized response creation methods - Add ResponseInterceptor to automatically inject timestamp and path - Add PaginationMeta to replace PageDto for pagination metadata - Add BatchResponse and DeleteResponse for specific operation types - Update ErrorResponse structure for consistency - Refactor GlobalExceptionHandler to use new response structure - Update all controllers to return ApiResponse wrapped responses: * AttendanceController * EmployeeController * LeaveController * PayrollController * UserController - Remove deprecated PageDto and PageMapper classes Benefits: - Consistent response format across all endpoints - Automatic timestamp and path injection via interceptor - Better separation of concerns with ResponseFactory pattern
1 parent 3a318d1 commit fd44d02

14 files changed

Lines changed: 308 additions & 142 deletions
Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package com.iodsky.motorph.attendance;
22

3-
import com.iodsky.motorph.common.PageDto;
4-
import com.iodsky.motorph.common.PageMapper;
3+
import com.iodsky.motorph.common.ApiResponse;
4+
import com.iodsky.motorph.common.ResponseFactory;
55
import jakarta.validation.Valid;
66
import jakarta.validation.constraints.Max;
77
import jakarta.validation.constraints.Min;
88
import lombok.RequiredArgsConstructor;
99
import org.springframework.data.domain.Page;
10-
import org.springframework.http.HttpStatus;
1110
import org.springframework.http.ResponseEntity;
1211
import org.springframework.security.access.prepost.PreAuthorize;
1312
import org.springframework.web.bind.annotation.*;
@@ -25,57 +24,63 @@ public class AttendanceController {
2524
private final AttendanceMapper attendanceMapper;
2625

2726
@PostMapping
28-
public ResponseEntity<AttendanceDto> createAttendance(@Valid @RequestBody(required = false) AttendanceDto attendanceDto) {
27+
public ResponseEntity<ApiResponse<AttendanceDto>> createAttendance(@Valid @RequestBody(required = false) AttendanceDto attendanceDto) {
2928
Attendance attendance = attendanceService.createAttendance(attendanceDto);
3029
AttendanceDto dto = attendanceMapper.toDto(attendance);
31-
return new ResponseEntity<>(dto, HttpStatus.CREATED);
30+
return ResponseFactory.created("Attendance created successfully", dto);
3231
}
3332

3433
@PreAuthorize("hasRole('HR')")
3534
@GetMapping
36-
public ResponseEntity<PageDto<AttendanceDto>> getAllAttendances(
37-
@RequestParam(defaultValue = "0") @Min(0) int page,
35+
public ResponseEntity<ApiResponse<List<AttendanceDto>>> getAllAttendances(
36+
@RequestParam(defaultValue = "0") @Min(0) int pageNo,
3837
@RequestParam(defaultValue = "10") @Min(1) @Max(100) int limit,
3938
@RequestParam(required = false) LocalDate startDate,
4039
@RequestParam(required = false) LocalDate endDate
4140
) {
42-
Page<Attendance> attendances = attendanceService.getAllAttendances(page, limit, startDate, endDate);
41+
Page<Attendance> page = attendanceService.getAllAttendances(pageNo, limit, startDate, endDate);
4342

44-
return ResponseEntity.ok(PageMapper.map(attendances, attendanceMapper::toDto));
43+
List<AttendanceDto> data = page.getContent().stream().map(attendanceMapper::toDto).toList();
44+
45+
return ResponseFactory.ok("Attendances retrieved successfully", data);
4546
}
4647

4748
@GetMapping("/me")
48-
public ResponseEntity<PageDto<AttendanceDto>> getMyAttendances(
49-
@RequestParam(defaultValue = "0") @Min(0) int page,
49+
public ResponseEntity<ApiResponse<List<AttendanceDto>>> getMyAttendances(
50+
@RequestParam(defaultValue = "0") @Min(0) int pageNo,
5051
@RequestParam(defaultValue = "10") @Min(1) @Max(100) int limit,
5152
@RequestParam(required = false) LocalDate startDate,
5253
@RequestParam(required = false) LocalDate endDate
5354
) {
54-
Page<Attendance> attendances = attendanceService
55-
.getEmployeeAttendances(page, limit, null, startDate, endDate);
55+
Page<Attendance> page = attendanceService
56+
.getEmployeeAttendances(pageNo, limit, null, startDate, endDate);
57+
58+
List<AttendanceDto> data = page.getContent().stream().map(attendanceMapper::toDto).toList();
5659

57-
return ResponseEntity.ok(PageMapper.map(attendances, attendanceMapper::toDto));
60+
return ResponseFactory.ok("Attendances retrieved successfully", data);
5861
}
5962

6063
@PreAuthorize("hasRole('HR')")
6164
@GetMapping("/employee/{id}")
62-
public ResponseEntity<PageDto<AttendanceDto>> getEmployeeAttendancesForHR(
65+
public ResponseEntity<ApiResponse<List<AttendanceDto>>> getEmployeeAttendancesForHR(
6366
@PathVariable Long id,
64-
@RequestParam(defaultValue = "0") @Min(0) int page,
67+
@RequestParam(defaultValue = "0") @Min(0) int pageNo,
6568
@RequestParam(defaultValue = "10") @Min(1) @Max(100) int limit,
6669
@RequestParam(required = false) LocalDate startDate,
6770
@RequestParam(required = false) LocalDate endDate
6871
) {
69-
Page<Attendance> attendances = attendanceService
70-
.getEmployeeAttendances(page, limit, id, startDate, endDate);
72+
Page<Attendance> page = attendanceService
73+
.getEmployeeAttendances(pageNo, limit, id, startDate, endDate);
74+
75+
List<AttendanceDto> data = page.getContent().stream().map(attendanceMapper::toDto).toList();
7176

72-
return ResponseEntity.ok(PageMapper.map(attendances, attendanceMapper::toDto));
77+
return ResponseFactory.ok("Attendances retrieved successfully", data);
7378
}
7479

7580
@PatchMapping("/{id}")
76-
public ResponseEntity<AttendanceDto> updateAttendance(@PathVariable UUID id, @Valid @RequestBody(required = false) AttendanceDto attendanceDto) {
81+
public ResponseEntity<ApiResponse<AttendanceDto>> updateAttendance(@PathVariable UUID id, @Valid @RequestBody(required = false) AttendanceDto attendanceDto) {
7782
Attendance attendance = attendanceService.updateAttendance(id, attendanceDto);
7883
AttendanceDto dto = attendanceMapper.toDto(attendance);
79-
return ResponseEntity.ok(dto);
84+
return ResponseFactory.ok("Attendance updated successfully", dto);
8085
}
8186
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.iodsky.motorph.common;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.Setter;
9+
10+
import java.time.Instant;
11+
12+
@Getter
13+
@Setter
14+
@Builder
15+
@AllArgsConstructor
16+
@JsonInclude(JsonInclude.Include.NON_NULL)
17+
@JsonPropertyOrder({
18+
"success",
19+
"message",
20+
"data",
21+
"meta",
22+
"timestamp",
23+
"path"
24+
})
25+
public class ApiResponse<T> {
26+
27+
private boolean success;
28+
private String message;
29+
private T data;
30+
private Instant timestamp;
31+
private String path;
32+
private PaginationMeta meta;
33+
34+
public ApiResponse(boolean success, String message, T data) {
35+
this.success = success;
36+
this.message = message;
37+
this.data = data;
38+
}
39+
40+
public ApiResponse(boolean success, String message, T data, PaginationMeta meta) {
41+
this.success = success;
42+
this.message = message;
43+
this.data = data;
44+
this.meta = meta;
45+
}
46+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.iodsky.motorph.common;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
5+
import lombok.NoArgsConstructor;
6+
7+
@Data
8+
@AllArgsConstructor
9+
@NoArgsConstructor
10+
public class BatchResponse {
11+
private Integer recordsCreated;
12+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.iodsky.motorph.common;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
import lombok.Setter;
8+
9+
@Getter
10+
@Setter
11+
@Builder
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public class DeleteResponse {
15+
private String resourceType;
16+
private Object resourceId;
17+
}
18+

src/main/java/com/iodsky/motorph/common/ErrorResponse.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111
@Data
1212
@Builder
1313
@AllArgsConstructor
14+
@JsonInclude(JsonInclude.Include.NON_NULL)
1415
public class ErrorResponse {
16+
1517
private Instant timestamp;
1618
private int status;
1719
private String message;
18-
19-
@JsonInclude(JsonInclude.Include.NON_NULL)
20+
private String path;
2021
private List<FieldValidationError> fieldErrors;
2122

2223
@Data

src/main/java/com/iodsky/motorph/common/PageDto.java

Lines changed: 0 additions & 18 deletions
This file was deleted.

src/main/java/com/iodsky/motorph/common/PageMapper.java renamed to src/main/java/com/iodsky/motorph/common/PaginationMeta.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
package com.iodsky.motorph.common;
22

3+
import lombok.Builder;
4+
import lombok.Getter;
35
import org.springframework.data.domain.Page;
46

5-
import java.util.function.Function;
7+
@Getter
8+
@Builder
9+
public class PaginationMeta {
610

7-
public class PageMapper {
8-
private PageMapper() {}
11+
private final int page;
12+
private final int size;
13+
private final long totalElements;
14+
private final int totalPages;
15+
private final boolean first;
16+
private final boolean last;
917

10-
public static <T, U>PageDto<U> map(Page<T> page, Function<T, U> mapper) {
11-
return PageDto.<U>builder()
12-
.content(page.getContent().stream().map(mapper).toList())
18+
public static PaginationMeta of(Page<?> page) {
19+
return PaginationMeta.builder()
1320
.page(page.getNumber())
1421
.size(page.getSize())
1522
.totalElements(page.getTotalElements())
@@ -18,5 +25,4 @@ public static <T, U>PageDto<U> map(Page<T> page, Function<T, U> mapper) {
1825
.last(page.isLast())
1926
.build();
2027
}
21-
2228
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.iodsky.motorph.common;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.http.ResponseEntity;
5+
6+
public class ResponseFactory {
7+
8+
private ResponseFactory() {}
9+
10+
public static <T> ResponseEntity<ApiResponse<T>> ok(String message, T data) {
11+
ApiResponse<T> res = new ApiResponse<>(
12+
true,
13+
message,
14+
data
15+
);
16+
return ResponseEntity.ok(res);
17+
}
18+
19+
public static <T> ResponseEntity<ApiResponse<T>> ok(String message, T data, PaginationMeta meta) {
20+
21+
ApiResponse<T> res = new ApiResponse<>(
22+
true,
23+
message,
24+
data,
25+
meta
26+
);
27+
28+
return ResponseEntity.ok(res);
29+
}
30+
31+
public static <T> ResponseEntity<ApiResponse<T>> created(String message, T data) {
32+
ApiResponse<T> res = new ApiResponse<>(
33+
true,
34+
message,
35+
data
36+
);
37+
return new ResponseEntity<>(res, HttpStatus.CREATED);
38+
}
39+
40+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.iodsky.motorph.common;
2+
3+
import org.springframework.core.MethodParameter;
4+
import org.springframework.http.MediaType;
5+
import org.springframework.http.converter.HttpMessageConverter;
6+
import org.springframework.http.server.ServerHttpRequest;
7+
import org.springframework.http.server.ServerHttpResponse;
8+
import org.springframework.web.bind.annotation.RestControllerAdvice;
9+
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
10+
11+
import java.time.Instant;
12+
13+
@RestControllerAdvice
14+
public class ResponseInterceptor implements ResponseBodyAdvice<Object> {
15+
16+
@Override
17+
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
18+
return true;
19+
}
20+
21+
@Override
22+
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
23+
Class<? extends HttpMessageConverter<?>> selectedConverterType,
24+
ServerHttpRequest request,
25+
ServerHttpResponse response
26+
) {
27+
28+
if (body instanceof ApiResponse<?> res) {
29+
res.setTimestamp(Instant.now());
30+
31+
String path = request.getURI().getRawPath();
32+
String query = request.getURI().getRawQuery();
33+
34+
res.setPath(query != null ? path + "?" + query : path);
35+
}
36+
37+
return body;
38+
}
39+
}

src/main/java/com/iodsky/motorph/common/exception/GlobalExceptionHandler.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.iodsky.motorph.common.exception;
22

33
import com.iodsky.motorph.common.ErrorResponse;
4+
import jakarta.servlet.http.HttpServletRequest;
45
import org.springframework.http.HttpStatus;
56
import org.springframework.http.ResponseEntity;
67
import org.springframework.web.bind.MethodArgumentNotValidException;
@@ -13,42 +14,45 @@
1314
public class GlobalExceptionHandler {
1415

1516
@ExceptionHandler(DuplicateFieldException.class)
16-
public ResponseEntity<ErrorResponse> handleDuplicateFieldException(DuplicateFieldException ex) {
17+
public ResponseEntity<ErrorResponse> handleDuplicateFieldException(DuplicateFieldException ex, HttpServletRequest request) {
1718
ErrorResponse error = ErrorResponse.builder()
1819
.timestamp(Instant.now())
1920
.status(ex.getStatus().value())
2021
.message(ex.getMessage())
22+
.path(request.getRequestURI())
2123
.build();
2224
return new ResponseEntity<>(error, ex.getStatus());
23-
2425
}
2526

2627
@ExceptionHandler(ApiException.class)
27-
public ResponseEntity<ErrorResponse> handleApiException(ApiException ex) {
28+
public ResponseEntity<ErrorResponse> handleApiException(ApiException ex, HttpServletRequest request) {
2829
ErrorResponse error = ErrorResponse.builder()
2930
.timestamp(Instant.now())
3031
.status(ex.getStatus().value())
3132
.message(ex.getMessage())
33+
.path(request.getRequestURI())
3234
.build();
3335
return new ResponseEntity<>(error, ex.getStatus());
3436
}
3537

3638
@ExceptionHandler(MethodArgumentNotValidException.class)
37-
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
39+
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request) {
3840
ErrorResponse error = ErrorResponse.builder()
3941
.timestamp(Instant.now())
4042
.status(HttpStatus.BAD_REQUEST.value())
4143
.message(ex.getMessage())
44+
.path(request.getRequestURI())
4245
.build();
4346
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
4447
}
4548

4649
@ExceptionHandler(Exception.class)
47-
public ResponseEntity<ErrorResponse> handleAllOtherException(Exception ex) {
50+
public ResponseEntity<ErrorResponse> handleAllOtherException(Exception ex, HttpServletRequest request) {
4851
ErrorResponse error = ErrorResponse.builder()
4952
.timestamp(Instant.now())
5053
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
5154
.message(ex.getMessage())
55+
.path(request.getRequestURI())
5256
.build();
5357
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
5458
}

0 commit comments

Comments
 (0)