BFF (Backend For Frontend) monolitin yeganə xarici API səthi. Heç bir modul birbaşa xaricə endpoint expose etmir — hamısı BFF-dən keçir.
bff/
├── auth/
│ ├── AuthBffController.java
│ ├── dto/
│ │ ├── RegisterRequest.java
│ │ ├── RegisterResponse.java
│ │ ├── LoginRequest.java
│ │ ├── LoginResponse.java
│ │ └── ...
│ └── mapper/
│ └── AuthBffMapper.java
├── user/
│ ├── UserBffController.java
│ ├── dto/
│ └── mapper/
├── car/
├── search/
├── messaging/
├── feed/
├── notification/
└── admin/
// YANLIŞ — qətiyyən etmə
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // Entity birbaşa çıxır!
}
// DOĞRU
@GetMapping("/users/{id}")
public UserProfileResponse getUser(@PathVariable Long id) {
User user = userService.findById(id);
return userBffMapper.toProfileResponse(user);
}
Eyni entity üçün fərqli endpoint-lərdə fərqli DTO-lar ola bilər:
UserProfileResponse — profil baxışı üçün (public data)UserSettingsResponse — settings ekranı üçün (private data daxil)UserSearchResultResponse — axtarış nəticəsi üçün (minimal data)@Mapper(componentModel = "spring")
public interface UserBffMapper {
@Mapping(target = "displayName", source = "name")
@Mapping(target = "memberSince", source = "createdAt")
@Mapping(target = "carCount", expression = "java(user.getCars().size())")
UserProfileResponse toProfileResponse(User user);
// Unmapped fields compile error verir — "extra field" riski sıfır
}
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
return JsonMapper.builder()
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.serializationInclusion(JsonInclude.Include.NON_NULL)
.build();
}
}
DTO class-larında:
@JsonInclude(JsonInclude.Include.NON_NULL)
public record UserProfileResponse(
Long id,
String displayName,
String username,
String city,
String memberSince,
Integer carCount,
Boolean isVerified
) {}
Privacy-ə görə fərqli field-lər:
// Mapper-da conditional mapping
@Mapping(target = "phone",
expression = "java(canSeePhone(viewer, target) ? target.getPhone() : null)")
UserProfileResponse toProfileResponse(User target, @Context User viewer);
// YANLIŞ
@Entity
@JsonIgnoreProperties({"password", "sessions"})
public class User { ... }
// DOĞRU — entity-də JSON annotation yoxdur
@Entity
public class User {
// Sadəcə JPA + validation annotations
}
MapStruct default-da unmapped target property üçün warning verir. Bunu error-a çeviririk:
@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR
)
Hər BFF endpoint üçün contract test:
@Test
void userProfileResponse_shouldContainOnlyExpectedFields() {
Set<String> allowedFields = Set.of(
"id", "displayName", "username", "city",
"memberSince", "carCount", "isVerified"
);
MvcResult result = mockMvc.perform(get("/api/v1/users/1"))
.andReturn();
JsonNode json = objectMapper.readTree(
result.getResponse().getContentAsString());
Set<String> actualFields = new HashSet<>();
json.fieldNames().forEachRemaining(actualFields::add);
assertEquals(allowedFields, actualFields,
"Response contains unexpected fields!");
}
@ArchTest
static final ArchRule entities_should_not_have_json_annotations =
noClasses()
.that().areAnnotatedWith(Entity.class)
.should().dependOnClassesThat()
.resideInAPackage("com.fasterxml.jackson..");
Bütün BFF endpoint-lər /api/v1/ prefix-i ilə başlayır:
POST /api/v1/auth/registerPOST /api/v1/auth/loginGET /api/v1/users/{id}/profileGET /api/v1/cars?plate=99KK777public record ErrorResponse(
String code, // "AUTH_001"
String message, // "Invalid OTP"
String timestamp, // ISO 8601
String path // "/api/v1/auth/verify-otp"
) {}
Heç vaxt stack trace, internal detail, və ya extra field qaytarılmır.
Hər BFF controller-da OpenAPI annotation:
@Operation(summary = "Get user profile")
@ApiResponse(responseCode = "200",
content = @Content(schema = @Schema(
implementation = UserProfileResponse.class)))
@GetMapping("/users/{id}/profile")
public UserProfileResponse getProfile(...) { ... }