Como crear un endpoint REST
Cinco piezas: controller, Request DTO, mapeo a Command, llamada al UseCase, Response DTO. Los controladores son package-private y los DTOs viven en clases separadas en el mismo paquete.
1. Crear el controller
Una clase package-private en {domainName}/adapter/in/web:
@Tag(name = ApiConstants.API_TAG)
@RestController
@RequestMapping("/v1/tasks")
@RequiredArgsConstructor
class CreateTaskController {
private final CreateTaskUseCase createTaskUseCase;
@Operation(summary = "Create a new task")
@PostMapping
ResponseEntity<String> create(@RequestBody @Valid CreateTaskRequest request) {
String id = createTaskUseCase.create(buildCommand(request));
return ResponseEntity.created(URI.create("/v1/tasks/" + id)).body(id);
}
private CreateTaskCommand buildCommand(CreateTaskRequest request) {
return new CreateTaskCommand(request.getTitle(), request.getType());
}
}
Puntos clave:
-
El controller depende del UseCase (input port), nunca del servicio.
-
/v1/taskscubre el path versionado;WebConfiganteponeAPI_PATH_PREFIX. Ver reference: path y versionado. -
@Tagagrupa el endpoint en OpenAPI;@Operationañade descripción.
|
Una clase por operación es la recomendación por defecto, no una regla dura. Dos operaciones cohesionadas pueden compartir controller — el caso típico son lecturas sobre el mismo recurso:
El criterio es cohesión (mismo recurso, mismo port, misma proyección), no la cuenta de métodos. |
2. Definir el Request DTO
Como clase aparte en el mismo paquete del controller:
@Data
public class CreateTaskRequest {
@NotBlank
private String title;
@NotNull
private IssueType type;
}
Anotar los campos con jakarta.validation.constraints.*. Una violación produce HTTP 400 con violations[] automáticamente — ver reference: validación.
3. Mapear Request → Command
El controller no llama al servicio con el DTO de transporte: traduce a Command (DTO de aplicación) y lo invoca:
private CreateTaskCommand buildCommand(CreateTaskRequest request) {
return new CreateTaskCommand(request.getTitle(), request.getType());
}
El Command vive en {domainName}/usecase/. El controller es el único punto que conoce ambos lados; el resto del dominio no sabe nada del DTO web.
4. Devolver la respuesta
POST de creación
201 Created con Location apuntando al nuevo recurso:
return ResponseEntity.created(URI.create("/v1/tasks/" + id)).body(id);
GET de detalle
Response DTO aparte con factory of(…):
@Data
public class TaskResponse {
private String id;
private String title;
public static TaskResponse of(Task task) {
TaskResponse response = new TaskResponse();
response.setId(task.getId());
response.setTitle(task.getTitle());
return response;
}
}
@GetMapping("/{id}")
ResponseEntity<TaskResponse> get(@PathVariable String id) {
Task task = getTaskDetailsUseCase.getDetails(id);
return ResponseEntity.ok(TaskResponse.of(task));
}
El factory of(…) a mano funciona bien para DTOs pequeños con pocos campos y mapeo 1-a-1. Cuando crece el número de campos o aparecen transformaciones no triviales (mapeo de enums, achatado de value objects, anidamientos) — usar MapStruct, que ya está en el stack — y declarar el mapper en su propia interfaz.
GET paginado
Pageable como último parámetro, PageResponse.of(…) para envolver el Page<T>:
@GetMapping
ResponseEntity<PageResponse<TaskResponse>> list(Pageable pageable) {
Page<Task> page = taskRetrievalPort.findAll(pageable);
return ResponseEntity.ok(PageResponse.of(page.map(TaskResponse::of)));
}
Para la forma exacta del payload paginado ver reference: paginación.
5. Probar el endpoint
X-Tenant-Id es obligatorio: sin él el TenantContextFilter no setea el contexto y los datasources multitenant fallan.
curl -X POST http://localhost:8080/v1/tasks \
-H 'Content-Type: application/json' \
-H 'X-Tenant-Id: 674b414f-27b5-461b-a8da-933326669018' \
-d '{"title":"Comprar pan","type":"CHORE"}'
Errores ya cubiertos sin código adicional en el controller:
-
Validación fallida →
400conviolations[]. -
ApplicationExceptionlanzada desde el caso de uso → status ycodedel enum.
Ver reference: errores para la tabla completa.
Resumen
| Pieza | Ubicación |
|---|---|
Controller ( |
|
Request DTO ( |
|
Response DTO ( |
|
Command ( |
|
Ver también
-
Como agregar un caso de uso — el contrato que el controller invoca.
-
Convenciones de la capa web — headers, paginación, validación, errores.
-
Tutorial: exponer el endpoint REST — el mismo flujo en el contexto del task manager.