Este guia mostra o passo a passo para criar endpoints REST seguindo o padrão do projeto.
Dois exemplos práticos:
POST /subscriptions— criar uma assinaturaGET /subscriptions/{id}— buscar uma assinatura por ID
Cada operação tem seu próprio subpacote dentro de adapter/in/rest/{dominio}/{operacao}/.
adapter/in/rest/subscription/
├── create/
│ ├── CreateSubscriptionController.java ← Controller
│ ├── CreateSubscriptionRestMapper.java ← Mapper (package-private)
│ └── dto/
│ └── SubscriptionRestInDTO.java ← DTO de entrada
└── get/
└── GetSubscriptionController.java ← Controller
usecase/in/subscription/
├── CreateSubscriptionUseCase.java ← Porta de entrada
└── GetSubscriptionUseCase.java ← Porta de entrada
application/model/domain/subscription/
└── CreateSubscriptionDomain.java ← Domain object
application/service/subscription/
├── CreateSubscriptionService.java ← Service
└── GetSubscriptionService.java ← Service
Fica em adapter/in/rest/{dominio}/{operacao}/dto/.
Usar sufixo RestInDTO. Usar @Builder @Data com @NotNull nos campos obrigatórios.
// adapter/in/rest/subscription/create/dto/SubscriptionRestInDTO.java
package br.com.jusfy.adapter.in.rest.subscription.create.dto;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.UUID;
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SubscriptionRestInDTO {
@NotNull
private UUID customerId;
@NotNull
private String status;
@NotNull
private LocalDateTime cycleStartDate;
@NotNull
private LocalDateTime cycleEndDate;
@NotNull
private Integer cycleNumber;
}Se ainda não existir para este domínio, criar em application/model/domain/{dominio}/.
Usar record. Os campos representam o contrato interno da aplicação.
// application/model/domain/subscription/CreateSubscriptionDomain.java
package br.com.jusfy.application.model.domain.subscription;
import java.time.LocalDateTime;
import java.util.UUID;
public record CreateSubscriptionDomain(
UUID id, // null quando criado via REST (gerado pelo JPA)
UUID customerId,
String status,
LocalDateTime cycleStartDate,
LocalDateTime cycleEndDate,
Integer cycleNumber
) {}Fica no mesmo pacote do controller. Classe final, package-private, com construtor privado.
Converte o DTO de entrada para o Domain.
// adapter/in/rest/subscription/create/CreateSubscriptionRestMapper.java
package br.com.jusfy.adapter.in.rest.subscription.create;
import br.com.jusfy.adapter.in.rest.subscription.create.dto.SubscriptionRestInDTO;
import br.com.jusfy.application.model.domain.subscription.CreateSubscriptionDomain;
final class CreateSubscriptionRestMapper {
private CreateSubscriptionRestMapper() {
}
static CreateSubscriptionDomain toDomain(SubscriptionRestInDTO in) {
if (in == null) return null;
return new CreateSubscriptionDomain(
null,
in.getCustomerId(),
in.getStatus(),
in.getCycleStartDate(),
in.getCycleEndDate(),
in.getCycleNumber()
);
}
}// usecase/in/subscription/CreateSubscriptionUseCase.java
package br.com.jusfy.usecase.in.subscription;
import br.com.jusfy.application.model.domain.subscription.CreateSubscriptionDomain;
import br.com.jusfy.application.model.entity.Subscription;
public interface CreateSubscriptionUseCase {
Subscription create(CreateSubscriptionDomain domain);
}Implementa a interface do Passo 4. Contém a regra de negócio.
// application/service/subscription/CreateSubscriptionService.java
package br.com.jusfy.application.service.subscription;
import br.com.jusfy.application.model.domain.subscription.CreateSubscriptionDomain;
import br.com.jusfy.application.model.entity.Subscription;
import br.com.jusfy.application.model.enumeration.SubscriptionStatusEnum;
import br.com.jusfy.usecase.in.subscription.CreateSubscriptionUseCase;
import br.com.jusfy.usecase.out.subscription.SubscriptionRepositoryUseCase;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
@Slf4j
public class CreateSubscriptionService implements CreateSubscriptionUseCase {
private final SubscriptionRepositoryUseCase subscriptionRepositoryUseCase;
@Override
public Subscription create(CreateSubscriptionDomain domain) {
log.info("START CREATING SUBSCRIPTION FOR CUSTOMER: {}", domain.customerId());
Subscription subscription = Subscription.builder()
.customerId(domain.customerId())
.status(SubscriptionStatusEnum.valueOf(domain.status()))
.cycleStartDate(domain.cycleStartDate())
.cycleEndDate(domain.cycleEndDate())
.cycleNumber(domain.cycleNumber() != null ? domain.cycleNumber() : 0)
.createdAt(LocalDateTime.now())
.updatedAt(LocalDateTime.now())
.build();
Subscription saved = subscriptionRepositoryUseCase.save(subscription);
log.info("SUBSCRIPTION CREATED SUCCESSFULLY: {}", saved.getId());
return saved;
}
}Injeta o usecase/in. Usa o Mapper. Retorna ResponseEntity.
// adapter/in/rest/subscription/create/CreateSubscriptionController.java
package br.com.jusfy.adapter.in.rest.subscription.create;
import br.com.jusfy.adapter.in.rest.subscription.create.dto.SubscriptionRestInDTO;
import br.com.jusfy.application.model.entity.Subscription;
import br.com.jusfy.usecase.in.subscription.CreateSubscriptionUseCase;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/subscriptions")
@RequiredArgsConstructor
public class CreateSubscriptionController {
private final CreateSubscriptionUseCase createSubscriptionUseCase;
@PostMapping(
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<Subscription> create(@Valid @RequestBody SubscriptionRestInDTO subscriptionRestInDTO) {
var domain = CreateSubscriptionRestMapper.toDomain(subscriptionRestInDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(createSubscriptionUseCase.create(domain));
}
}curl -X POST http://localhost:8080/subscriptions \
-H "Content-Type: application/json" \
-d '{
"customerId": "550e8400-e29b-41d4-a716-446655440001",
"status": "ACTIVE",
"cycleStartDate": "2026-03-16T00:00:00",
"cycleEndDate": "2026-04-16T00:00:00",
"cycleNumber": 1
}'Para endpoints de busca, o fluxo é mais simples — não há DTO de entrada nem Mapper.
// usecase/in/subscription/GetSubscriptionUseCase.java
package br.com.jusfy.usecase.in.subscription;
import br.com.jusfy.application.model.entity.Subscription;
import java.util.UUID;
public interface GetSubscriptionUseCase {
Subscription findById(UUID id);
}// application/service/subscription/GetSubscriptionService.java
package br.com.jusfy.application.service.subscription;
import br.com.jusfy.application.exception.BusinessException;
import br.com.jusfy.application.model.entity.Subscription;
import br.com.jusfy.usecase.in.subscription.GetSubscriptionUseCase;
import br.com.jusfy.usecase.out.subscription.SubscriptionRepositoryUseCase;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.UUID;
@Service
@RequiredArgsConstructor
@Slf4j
public class GetSubscriptionService implements GetSubscriptionUseCase {
private final SubscriptionRepositoryUseCase subscriptionRepositoryUseCase;
@Override
public Subscription findById(UUID id) {
log.info("FINDING SUBSCRIPTION: {}", id);
return subscriptionRepositoryUseCase.findById(id)
.orElseThrow(() -> new BusinessException("Subscription not found"));
}
}Sem DTO nem Mapper. Path variable direto para o use case.
// adapter/in/rest/subscription/get/GetSubscriptionController.java
package br.com.jusfy.adapter.in.rest.subscription.get;
import br.com.jusfy.application.model.entity.Subscription;
import br.com.jusfy.usecase.in.subscription.GetSubscriptionUseCase;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@RestController
@RequestMapping("/subscriptions")
@RequiredArgsConstructor
public class GetSubscriptionController {
private final GetSubscriptionUseCase getSubscriptionUseCase;
@GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Subscription> findById(@PathVariable UUID id) {
return ResponseEntity.ok(getSubscriptionUseCase.findById(id));
}
}curl http://localhost:8080/subscriptions/550e8400-e29b-41d4-a716-446655440000| Regra | Detalhe |
|---|---|
| DTO de entrada | Sufixo RestInDTO, @Builder @Data, @NotNull nos obrigatórios |
| Mapper | Classe final package-private, método static, mesmo pacote do controller |
| Controller | @Valid no body, ResponseEntity, consumes/produces explícitos |
| UseCase | Interface em usecase/in, nunca injetar o service diretamente no controller |
| Service | @Service @RequiredArgsConstructor @Slf4j, implementa o use case |
| Dependências do service | Apenas interfaces de usecase/out, nunca adapters concretos |
- DTO em
adapter/in/rest/{dominio}/{operacao}/dto/com sufixoRestInDTO - Mapper em
adapter/in/rest/{dominio}/{operacao}/(classefinal, package-private) - Interface em
usecase/in/{dominio}/ - Domain record em
application/model/domain/{dominio}/(se necessário) - Service em
application/service/{dominio}/implementando a interface - Controller em
adapter/in/rest/{dominio}/{operacao}/ - Testes unitários do service
- Interface em
usecase/in/{dominio}/ - Service em
application/service/{dominio}/implementando a interface - Controller em
adapter/in/rest/{dominio}/{operacao}/ - Testes unitários do service