Índice
- Servicios Web RESTful (I) - Introducción
- Servicios Web RESTful (II) - API-First vs Code-First
- Servicios Web RESTful (III) - OpenAPI
- Servicios Web RESTful (IV) - OpenAPI Generator
Introducción
OpenAPI Generator es una herramienta que permite a los desarrolladores generar código cliente, servidor, documentación y configuraciones a partir de especificaciones OpenAPI. Esta herramienta facilita la creación de APIs robustas y bien documentadas, acelerando el proceso de desarrollo y asegurando la consistencia en la implementación.
OpenAPI Generator surgió como una bifurcación de Swagger Codegen en 2018. Desde entonces, ha crecido significativamente, incorporando soporte para más lenguajes de programación y frameworks, y mejorando la flexibilidad y personalización en la generación de código. La comunidad detrás de OpenAPI Generator ha contribuido con numerosas mejoras y extensiones, haciendo de esta herramienta una opción preferida para muchos desarrolladores.
Instalación y Configuración
Para usarlo existen múltiples opciones, aunque para el caso actual usaremos maven, pero igualmente se incluyen algunas otras:
- npm
npm install @openapitools/openapi-generator-cli -g
- Homebrew
brew install openapi-generator
- Scoop
scoop install openapi-generator-cli
- PyPI
pip install openapi-generator-cli
- Docker
docker run --rm \
-v ${PWD}:/local openapitools/openapi-generator-cli generate \
-i /local/petstore.yaml \
-g go \
-o /local/out/go
- JAR
wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.12.0/openapi-generator-cli-7.12.0.jar -O openapi-generator-cli.jar
- maven (también existe la versión para gradle)
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.12.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec>
<generatorName>java</generatorName>
<configOptions>
<sourceFolder>src/gen/java/main</sourceFolder>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
Caso práctico
Partiendo del ejemplo de api del artículo anterior se va a crear un proyecto servidor y uno cliente que sirvan y consuman respectivamente la api.
Generación de los proyectos
Para crear cada uno de ellos se accede a la web de spring initializr:
Servidor

Cliente

Implementando cliente y servidor
Para el cliente se va a utilizar feign, mientras que para el servidor se utilizará un controlador que implemente la api,
más información sobre feign en spring-cloud-openfeign.
En ambos casos para generar el código a partir del contrato con maven se añade el siguiente plugin:
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>${openapi.generator.version}</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/api.yml</inputSpec>
<generateSupportingFiles>false</generateSupportingFiles>
<generatorName>spring</generatorName>
<configOptions>
<useSpringBoot3>true</useSpringBoot3>
<interfaceOnly>true</interfaceOnly>
<skipDefaultInterface>true</skipDefaultInterface>
<useResponseEntity>false</useResponseEntity>
<apiPackage>com.example.openapi.server.api</apiPackage>
<modelPackage>com.example.openapi.server.model</modelPackage>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
- inputSpec: ruta al fichero yml que contiene el contrato de la api.
- generateSupportingFiles: como, tanto para feign como para el controlador, solo es necesario la interfaz y los modelos, con esto se evita la creación de clases de apoyo.
- generatorName: generador que se va a utilizar a la hora de crear el código en este caso spring.
- configOptions: opciones adicionales.
- useSpringBoot3: si no se utilizase se generaría para spring boot 2.
- interfaceOnly: en la misma línea de generar solo interfaces y modelos.
- skipDefaultInterface: evita que las interfaces tengan una implementación default.
- useResponseEntity: las respuestas de los métodos son directamente los modelos en lugar de un ResponseEntity
. - apiPackage: paquetería donde generar la api.
- modelPackage: paquetería donde generar los modelos.
Más información sobre los parámetros que se pueden utilizar en openapi-generator/spring y
openapi-generator/README.md.
Aparte del plugin para que el código generado funcione correctamente son necesarias las siguientes librerías:
- spring-boot-starter-validation
- swagger-annotations
- jackson-databind-nullable
Una vez generado el código tendremos lo siguiente:

Como se puede observar tenemos la interfaz UsersApi y los modelos Error y User que coinciden con lo que se definión en la api bajo components.
Implementando el servidor
Con todo ya preparado se va a crear el controlador UsersController que implementará la interfaz UsersApi con lo que ya se podrá crear el código necesario para dar respuesta a los distintos servicios del contrato.
Una posible implementación (básica solo para cubrir las operaciones) podría ser:
@RestController
public class UsersController implements UsersApi {
private final Map<Long, User> users = new HashMap<>();
@Override
public User createOrUpdate(@Valid User user) {
users.put(user.getId(), user);
return user;
}
@Override
public List<User> read() {
return new ArrayList<>(users.values());
}
@Override
public User readOne(Long id) {
return Optional.ofNullable(users.get(id)).orElseThrow(() -> new UserNotFoundException());
}
}
Destacar simplemente el que se lance una excepción personalizada UserNotFoundException para tener más control sobre el error que se quiere devolver acorde al contracto. Esto hace que sea necesario la utilización de un RestControllerAdvice como handler de la api para devolver los errores correspondientes. Una implementación básica podría ser la siguiente:
@RestControllerAdvice
public class ApiHandler {
private final String USER_NOT_FOUND_DESCRIPTION = "User not found";
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(UserNotFoundException.class)
public Error handleUserNotFoundException() {
return new Error(CodeEnum._400_04, USER_NOT_FOUND_DESCRIPTION);
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public Error handleException(final Exception e) {
return new Error(CodeEnum._500_00, e.getMessage());
}
}
Por último sobre el servidor añadir que se publica por defecto en el puerto 8080 y que como context path se expone /api acorde a lo definido en el yml, para ello el fichero application.yml contiene:
server:
servlet:
context-path: /api
Implementando el cliente
En este caso se va a crear una clase que contenga la configuración necesaria de Feign para la creación del cliente que
nos permita realizar las correspondientes llamadas al servidor.
Un ejemplo de ello sería:
@Configuration
public class ApiConfig {
@Bean
Contract feignContract() {
return new SpringMvcContract();
}
@Bean
Encoder feignEncoder() {
var jsonMessageConverters = new MappingJackson2HttpMessageConverter(new ObjectMapper());
return new SpringEncoder(() -> new HttpMessageConverters(jsonMessageConverters));
}
@Bean
Decoder feignDecoder() {
var jsonMessageConverters = new MappingJackson2HttpMessageConverter(new ObjectMapper());
return new ResponseEntityDecoder(new SpringDecoder(() ->
new HttpMessageConverters(jsonMessageConverters)));
}
@Bean
UsersApi userClient(Contract contract, Decoder decoder, Encoder encoder) {
return Feign.builder()
.contract(contract)
.decoder(decoder)
.encoder(encoder)
.target(UsersApi.class, "http://localhost:8080/api");
}
}
- @Configuration: anotación de spring para clases de configuración donde se definen distintos beans.
- @Bean: anotación que genera un bean con lo devuelto por el método.
- Contract, Encoder y Decoder: beans necesarios para que el cliente de Feign pueda tratar la información enviada y
recibida del servicio. - UsersApi: cliente de la api construida con Feign.builder el cual necesita como parámetros los bean definidos en el punto anterior y la url destino junto a la clase que contiene el interfaz con los métodos expuestos por el servicio.
Por último, para comprobar que el cliente es correcto, se implementan llamadas al servicio con validaciones sobre lo que se espera recibir en cada caso. Para ello:
@SpringApplication
public class ClientApplication implements CommandLineRunner {
@Autowired
UsersApi client; // cliente generado con Feign
public static void main(String[] args) {
SpringApplication.run(ClientApplication.class, args);
}
@Override
public void run(String... args) throws Exception { //método con llamadas a las distintas operaciones
// Call read that returns 0 users
Assert.isTrue(client.read()!=null, "Read method has users");
System.out.println("Call to read service --> OK");
// Call read one user that throws Not Found Exception
try {
client.readOne(0L);
System.err.println("Read user must throw a exception");
} catch (Exception e) {
Assert.isTrue(e instanceof FeignException.NotFound, "Wrong exception or message in read user");
System.out.println("Call to read user throws FeignException.NotFound --> OK");
}
// Call put user
final User user = new User(1L, "John Doe");
Assert.isTrue(user.equals(client.createOrUpdate(user)), "User is not the same as createOrUpdated");
System.out.println("Call to put user --> OK");
// Call read one user that returns the previous user
Assert.isTrue(user.equals(client.readOne(user.getId())), "User is not the same as sending before");
System.out.println("Call to read user (previous inserted) --> OK");
}
}
Conclusión
Con este último punto se ha visto como poder implementar un cliente y un servidor que cumplan con el contrato previamente definido, comentar que esta implentación se podría haber realizado con otros lenguajes o frameworks, un listado completo de las opciones actuales se puede encontrar aquí.
Referencias
Los proyectos al completo se pueden descargar de github.