API REST para la gestión de recetas de cocina desarrollada para el programa MasterChef. Permite a participantes, chefs y televidentes registrar, consultar y gestionar recetas de cocina de manera interactiva.
- Descripción del Proyecto
- Tecnologías Utilizadas
- Características Principales
- Arquitectura
- Instalación y Ejecución Local
- Configuración
- Endpoints de la API
- Ejemplos de Request y Response
- Documentación Swagger
- Testing
- CI/CD
Recipe Management API es una API REST desarrollada para DOSW Company como parte de un proyecto para un programa de telerrealidad de cocina. La aplicación permite:
- Televidentes: Compartir sus propias recetas
- Participantes: Registrar recetas del programa con información de temporada
- Chefs jurados: Publicar recetas profesionales
Un importante programa de telerrealidad de cocina necesita un sitio web donde los espectadores puedan:
- Consultar recetas que han aparecido en las temporadas del programa
- Aprender y replicar las recetas en casa
- Contribuir con sus propias recetas de manera interactiva
Cada receta incluye:
- Título descriptivo
- Lista de ingredientes
- Pasos de preparación detallados
- Nombre del chef (participante, jurado o televidente)
- Información de temporada (para participantes)
- Número consecutivo único
- Timestamps de creación y actualización
- Java 21 - Lenguaje de programación principal
- Spring Boot 3.3.0 - Framework de desarrollo
- Maven - Gestión de dependencias y build
- MongoDB Atlas - Base de datos NoSQL en la nube
- SpringDoc OpenAPI 3 - Generación automática de documentación
- Swagger UI - Interfaz interactiva para la API
- JUnit 5 - Framework de testing
- Mockito - Mocking para tests unitarios
- Spring Boot Test - Testing de integración
- Jacoco - Covertura de pruebas unitarias
- GitHub Actions - CI/CD pipelines
- Azure App Service - Hosting en la nube
- Lombok - Reducción de código
- Registrar receta de televidente
- Registrar receta de participante (con temporada)
- Registrar receta de chef
- Obtener todas las recetas
- Obtener receta por número consecutivo
- Filtrar recetas de participantes
- Filtrar recetas de televidentes
- Filtrar recetas de chefs
- Obtener recetas por temporada
- Buscar recetas por ingrediente
- Eliminar receta
- Actualizar receta
Antes de comenzar, asegúrate de tener instalado:
- Java 21 o superior
- Maven 3.8+
- MongoDB Atlas
- Git
- Un editor de código (recomendado: IntelliJ IDEA, VS Code)
# Verificar Java
java -version
# Salida esperada: openjdk version "21.x.x"
# Verificar Maven
mvn -version
# Salida esperada: Apache Maven 3.8.x o superior
# Verificar Git
git --version
# Salida esperada: git version 2.x.xgit clone https://github.com/AlejandroHenao2572/recipe-management-api.git
cd recipe-management-api- Ve a MongoDB Atlas
- Crea un cluster gratuito
- Haz clic en "Connect" → "Connect your application"
- Copia la cadena de conexión:
mongodb+srv://<username>:<password>@<cluster>.mongodb.net/recipedb?retryWrites=true&w=majority - En "Network Access", agrega tu IP
- Editar
application.properties# src/main/resources/application.properties spring.data.mongodb.uri=mongodb+srv://username:password@cluster.mongodb.net/recipedb?retryWrites=true&w=majority
Importante: Reemplaza username, password y cluster con tus credenciales reales.
mvn clean installmvn spring-boot:runLa aplicación estará disponible en: http://localhost:8080
Abre tu navegador y ve a:
- Swagger UI: http://localhost:8080/swagger-ui.html
- API Docs: http://localhost:8080/api-docs
- Local:
http://localhost:8080/api/recipes - Producción:
https://recipe-api-doswcompany.azurewebsites.net/api/recipes
| Método | Endpoint | Descripción |
|---|---|---|
POST |
/viewer |
Registrar receta de televidente |
POST |
/contestant |
Registrar receta de participante |
POST |
/chef |
Registrar receta de chef |
GET |
/ |
Obtener todas las recetas |
GET |
/{consecutiveNumber} |
Obtener receta por número |
GET |
/contestant |
Obtener recetas de participantes |
GET |
/viewer |
Obtener recetas de televidentes |
GET |
/chef |
Obtener recetas de chefs |
GET |
/season/{season} |
Obtener recetas por temporada |
GET |
/search?ingredient={ingrediente} |
Buscar por ingrediente |
PUT |
/{consecutiveNumber} |
Actualizar receta |
DELETE |
/{consecutiveNumber} |
Eliminar receta |
Endpoint: POST /api/recipes/viewer:

Endpoint: POST /api/recipes/contestant


Endpoint: POST /api/recipes/chef


La API cuenta con documentación Swagger desplegada en Azure:
Swagger UI (Azure):
https://recipe-api-doswcompany-encfd2f4ekbyhrhv.canadacentral-01.azurewebsites.net/swagger-ui/index.html
Swagger UI (Local):
http://localhost:8080/swagger-ui.html
- Documentación completa de todos los endpoints
- Ejemplos de request y response
- Pruebas en tiempo real desde el navegador
- Esquemas de datos (DTOs y modelos)
- Códigos de estado HTTP documentados
- Validaciones
- Validar que se pueda registrar una receta
@Test
@DisplayName("Should register viewer recipe successfully")
void shouldRegisterViewerRecipeSuccessfully() throws Exception {
// Given
when(recipeService.registerViewerRecipe(any(ViewerRecipeRequestDto.class))).thenReturn(mockRecipe);
// When & Then
mockMvc.perform(post("/api/recipes/viewer")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(viewerRecipeRequestDto)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id", is("12345")))
.andExpect(jsonPath("$.title", is("Test Recipe")))
.andExpect(jsonPath("$.chefName", is("Test Chef")))
.andExpect(jsonPath("$.recipeType", is("VIEWER")))
.andExpect(jsonPath("$.consecutiveNumber", is(1)));
verify(recipeService).registerViewerRecipe(any(ViewerRecipeRequestDto.class));
}
@Test
@DisplayName("Should register contestant recipe successfully")
void shouldRegisterContestantRecipeSuccessfully() throws Exception {
// Given
mockRecipe.setRecipeType("CONTESTANT");
mockRecipe.setSeason(3);
when(recipeService.registerContestantRecipe(any(ContestantRecipeRequestDto.class))).thenReturn(mockRecipe);
// When & Then
mockMvc.perform(post("/api/recipes/contestant")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(contestantRecipeRequestDto)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id", is("12345")))
.andExpect(jsonPath("$.recipeType", is("CONTESTANT")))
.andExpect(jsonPath("$.season", is(3)));
verify(recipeService).registerContestantRecipe(any(ContestantRecipeRequestDto.class));
}
@Test
@DisplayName("Should register chef recipe successfully")
void shouldRegisterChefRecipeSuccessfully() throws Exception {
// Given
mockRecipe.setRecipeType("CHEF");
when(recipeService.registerChefRecipe(any(ChefRecipeRequestDto.class))).thenReturn(mockRecipe);
// When & Then
mockMvc.perform(post("/api/recipes/chef")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(chefRecipeRequestDto)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id", is("12345")))
.andExpect(jsonPath("$.recipeType", is("CHEF")));
verify(recipeService).registerChefRecipe(any(ChefRecipeRequestDto.class));
}
@Test
@DisplayName("Should return bad request when viewer recipe has invalid data")
void shouldReturnBadRequestWhenViewerRecipeHasInvalidData() throws Exception {
// Given
viewerRecipeRequestDto.setTitle(""); // Invalid title
viewerRecipeRequestDto.setIngredients(Collections.emptyList()); // Invalid ingredients
// When & Then
mockMvc.perform(post("/api/recipes/viewer")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(viewerRecipeRequestDto)))
.andExpect(status().isBadRequest());
verify(recipeService, never()).registerViewerRecipe(any());
}
- Validar que la búsqueda por ingrediente devuelva resultados correctos
@Test
@DisplayName("Should search recipes by ingredient successfully")
void shouldSearchRecipesByIngredientSuccessfully() throws Exception {
// Given
String ingredient = "tomate";
List<Recipe> foundRecipes = Arrays.asList(mockRecipe, new Recipe());
when(recipeService.searchRecipesByIngredient(ingredient)).thenReturn(foundRecipes);
// When & Then
mockMvc.perform(get("/api/recipes/search")
.param("ingredient", ingredient))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].id", is("12345")));
verify(recipeService).searchRecipesByIngredient(ingredient);
}
@Test
@DisplayName("Should return 404 when no recipes found by ingredient")
void shouldReturn404WhenNoRecipesFoundByIngredient() throws Exception {
// Given
String ingredient = "ingrediente-inexistente";
when(recipeService.searchRecipesByIngredient(ingredient))
.thenThrow(new MasterChefException("No se encontraron recetas con el ingrediente: " + ingredient));
// When & Then
mockMvc.perform(get("/api/recipes/search")
.param("ingredient", ingredient))
.andExpect(status().isNotFound());
verify(recipeService).searchRecipesByIngredient(ingredient);
}
- Validar que se devuelva error si se consulta una receta inexistente
@DisplayName("Should return 404 when recipe not found by consecutive number")
void shouldReturn404WhenRecipeNotFoundByConsecutiveNumber() throws Exception {
// Given
Long consecutiveNumber = 999L;
when(recipeService.getRecipeByConsecutiveNumber(consecutiveNumber))
.thenThrow(new MasterChefException("No se encontró la receta con número consecutivo: " + consecutiveNumber));
// When & Then
mockMvc.perform(get("/api/recipes/{consecutiveNumber}", consecutiveNumber))
.andExpect(status().isNotFound());
verify(recipeService).getRecipeByConsecutiveNumber(consecutiveNumber);
}
# Ejecutar todos los tests
mvn test
# Ejecutar tests con cobertura (JaCoCo)
mvn clean test jacoco:reportDespués de ejecutar los tests con JaCoCo:
# El reporte HTML estará en:
open target/site/jacoco/index.htmlEl proyecto incluye dos workflows de CI/CD:
Trigger: Push o Pull Request a la rama develop
Funciones:
- Ejecuta tests automáticamente
- Valida que el código compile
# Ubicación: .github/workflows/ci.yml
# Se ejecuta en: push/PR a developTrigger: Push a la rama main
Funciones:
- Compila la aplicación
- Empaqueta el JAR
- Despliega automáticamente en Azure
# Ubicación: .github/workflows/deploy-azure.yml
# Se ejecuta en: push a main

















