Skip to content

Latest commit

 

History

History
389 lines (289 loc) · 11.7 KB

File metadata and controls

389 lines (289 loc) · 11.7 KB

Guia — Criando um Novo Endpoint REST

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 assinatura
  • GET /subscriptions/{id} — buscar uma assinatura por ID

Visão Geral dos Arquivos

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

Exemplo 1 — POST /subscriptions

Passo 1 — Criar o DTO de entrada

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;
}

Passo 2 — Criar o Domain object

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
) {}

Passo 3 — Criar o Mapper

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()
        );
    }
}

Passo 4 — Criar a interface usecase/in

// 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);
}

Passo 5 — Criar o Service

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;
    }
}

Passo 6 — Criar o Controller

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));
    }
}

Testando

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
  }'

Exemplo 2 — GET /subscriptions/{id}

Para endpoints de busca, o fluxo é mais simples — não há DTO de entrada nem Mapper.

Passo 1 — Criar a interface usecase/in

// 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);
}

Passo 2 — Criar o Service

// 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"));
    }
}

Passo 3 — Criar o Controller

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));
    }
}

Testando

curl http://localhost:8080/subscriptions/550e8400-e29b-41d4-a716-446655440000

Regras Gerais

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

Checklist — POST

  • DTO em adapter/in/rest/{dominio}/{operacao}/dto/ com sufixo RestInDTO
  • Mapper em adapter/in/rest/{dominio}/{operacao}/ (classe final, 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

Checklist — GET

  • 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