Exponer el endpoint REST
En el paso anterior conectaste CreateTaskUseCase con TaskService. Hasta aqui el caso de uso solo es accesible desde codigo. Ahora vas a exponerlo por HTTP para que clientes externos puedan crear tareas.
El scaffold ya te dejo un CreateTaskController esqueleto en task.adapter.in.web y un CreateTaskRequest vacio en el mismo paquete. En este paso vas a:
-
Llenar el Request DTO con los campos que el cliente envia.
-
Completar el mapeo Request → Command.
-
Probar el endpoint con
curly verificar las validaciones.
1. Completar el Request DTO
El Request necesita los mismos campos que tu Command — title y type. Anotalos con jakarta.validation.constraints.*:
@Data
public class CreateTaskRequest {
@NotBlank
private String title;
@NotNull
private IssueType type;
}
@NotBlank rechaza titulos vacios o solo whitespace. @NotNull exige el tipo. Si una validacion falla, Spring devuelve 400 Bad Request con la lista de violaciones — no necesitas codigo de manejo de errores en el controller.
|
Por que un Request separado del Command si hoy tienen los mismos campos. El Request es el DTO HTTP (lo que llega del cliente). El Command es el DTO de aplicacion (lo que entiende el caso de uso). Hoy son iguales — pero el dia que el JSON cambie sin romper la operacion (recibir |
2. Mapear Request → Command
El controller no llama al servicio con el Request — lo traduce a CreateTaskCommand. El buildCommand que te dejo el scaffold devuelve un Command vacio. Corrigelo:
private CreateTaskCommand buildCommand(CreateTaskRequest request) {
return new CreateTaskCommand(request.getTitle(), request.getType());
}
Para que el constructor con dos parametros funcione, agrega @AllArgsConstructor al CreateTaskCommand que armaste en el paso anterior:
@Data
@AllArgsConstructor
public class CreateTaskCommand {
private String title;
private IssueType type;
}
El controller, una vez completo, queda asi:
@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());
}
}
Observaciones:
-
La clase es package-private — los consumidores externos hablan via HTTP, no via Java.
-
@RequestMapping("/v1/tasks")cubre la version.WebConfigantepone elapiBasePathque diste al generar el proyecto (/tasks), asi que la URL real es/tasks/v1/tasks. -
El controller depende de la interfaz
CreateTaskUseCase, no deTaskService.
3. Probar el endpoint
Arranca la app:
mvn spring-boot:run
Llama al endpoint. X-Tenant-Id es obligatorio — sin el, el TenantContextFilter no resuelve contexto y los datasources fallan:
curl -i -X POST http://localhost:8080/tasks/v1/tasks \
-H 'Content-Type: application/json' \
-H 'X-Tenant-Id: 674b414f-27b5-461b-a8da-933326669018' \
-d '{"title":"Comprar pan","type":"CHORE"}'
Respuesta esperada:
HTTP/1.1 201 Created
Location: /v1/tasks/K3jBLm9xQp1
Content-Type: text/plain
K3jBLm9xQp1
El id que devuelve es el AlphaID que Task.create(…) asigno antes del save(). El header Location apunta al recurso recien creado — el GetTaskController lo sirve, ya scaffoldeado con la misma estructura del Create.
Ahora prueba una validacion — un titulo vacio:
curl -i -X POST http://localhost:8080/tasks/v1/tasks \
-H 'Content-Type: application/json' \
-H 'X-Tenant-Id: 674b414f-27b5-461b-a8da-933326669018' \
-d '{"title":"","type":"CHORE"}'
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Request validation failed.",
"violations": [
{ "field": "title", "message": "must not be blank" }
]
}
Sin un solo try/catch en el controller — GlobalExceptionHandler lo cubre globalmente.
Que queda fuera de este paso
-
GET de detalle —
GetTaskControllerya esta scaffoldeado con la misma forma (@PathVariable String id, unTaskResponsecon factoryof(…), yResponseEntity.ok(…)). Ver GET de detalle en el how-to. -
Listado paginado —
Pageable+PageResponse. No hay scaffold para esto. Ver GET paginado en el how-to. -
Errores de negocio — hoy solo validaste casos de input invalido. Cuando el caso de uso lance una
ApplicationException(recurso no encontrado, invariante violada), el handler global la mapea a HTTP. Ver reference: errores.
Siguiente paso
Tu API ya acepta requests y devuelve 201. En el proximo capitulo vas a agregar persistencia con JPA.