Vamos a aprender a cómo crear una API REST con Java Servlets desde cero usando Arquitectura Hexagonal. Vamos a construir una API REST pequeña pero con una arquitectura profesional.
Al finalizar habrás entendido:
- Cómo funciona un Servlet.
- El ciclo de vida de un Servlet.
- Cómo recibe las peticiones HTTP.
- Cómo responder en formato JSON.
- Cómo organizar un proyecto con Arquitectura Hexagonal.
- Cómo separar la lógica de negocio del Servlet.
Nota: este es un ejercicio de aprendizaje, ya que Spring Boot o Micronaut mejoran la expericia de trabajo y desarrollo de aplicaciones modernas.
Primeros pasos antes de iniciar
Configurar git
Procedemos a preparar el entono Git en donde almacenaremos el proyecto, para esto vamos a configurar varias claves privadas SSH para diferentes repositorio git.
Procedemos a crear el repositorio Git con el nombre de contact-api.
Puede encontrar el proyecto en https://github.com/codigoelectronica/contact-api
Configurar Java y Maven
Vamos a trabajar con Java 17, te explico como instalarlo en: Cómo instalar Java en cualquier sistema operativo.
Luego instalamos Maven, te lo explico en el posts de instalar maven.
Configurar VScode
Procedemos a instalar VC code con las exenciones necesarias para Java, ademas de configurar vscode con su entrono de trabajo.
Crear el proyecto base
Vamos a trabajar con un archetype web con maven y java, aunque lo explicare a detalle en este posts, revisa la documentación ya creada para que entienda el paso a paso.
Estructura del proyecto
El proyecto es un API REST con un CRUD básico para crear contactos telefónicos, no vamos a realizar un API completo, vamos a crear las entradas básicas.
Este posts tiene el objetivo de entender como funcionan los Servlets en un proyecto Java.
El CRUD
Nuestra API ya soporta:
| Método | Ruta | Estado HTTP |
|---|---|---|
| GET | /contacts |
200 |
| GET | /contacts/{id} |
200 / 404 |
| POST | /contacts |
201 |
| PUT | /contacts/{id} |
200 / 404 |
| DELETE | /contacts/{id} |
204 / 404 |
Estructura
Crear el proyecto
Desde una terminal de vscode ejecuta:
mvn archetype:generate \
-DgroupId=com.example.contacts \
-DartifactId=contact-api \
-DarchetypeArtifactId=maven-archetype-webapp \
-DinteractiveMode=false
maven-archetype-webapp: crea automáticamente una estructura preparada para aplicaciones web basadas en Servlets.
Crea la siguiente estructura
contact-api
├── pom.xml
└── src
└── main
└── webapp
└── WEB-INF
web.xml
De esta estructura vamos a agregar las clases necesarias para poder tener el API funcional.
Crear los paquetes
Dentro de src/main/java procedemos a crear la siguiente estructura de paquetes:
contact-api
└── src/main/com/example/contacts
└── adapter
└── servlet
└── application
└── config
└── domain
└── infraestructura
└── repository
Configurar el pom.xml
Vamos a añadir varios elementos al pom, principalmente el soporte para servlet y Jackson, revisa el archivo final en: https://github.com/codigoelectronica/contact-api/blob/main/pom.xml
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.2</version>
</dependency>
¿Por qué packaging es war?
En las aplicaciones Servlet existen dos tipos principales de empaquetado:
jar:Se usa normalmente con frameworks que incluyen su propio servidor embebido, como Spring Boot o Micronaut.war: Se utiliza cuando la aplicación será desplegada en un contenedor Servlet, como Tomcat.
Como queremos comprender el funcionamiento clásico de los Servlets, utilizaremos un archivo WAR.
Crear el modelo Contact
Dentro de com.example.contacts.domain; https://github.com/codigoelectronica/contact-api/blob/main/src/main/java/com/example/contacts/domain/Contact.java
public class Contact {
private Long id;
private String name;
private String email;
private String phone;
// Constructor
// getters y setter
}
¿Por qué este objeto pertenece al dominio?
Porque representa un concepto del negocio.
No importa si mañana usas:
- Tomcat
- Spring
- Micronaut
- Quarkus
- AWS Lambda
El contacto seguirá existiendo.
Ese es precisamente el propósito del dominio.
Crear el contrato del repositorio
Al revisar el archivo https://github.com/codigoelectronica/contact-api/blob/main/src/main/java/com/example/contacts/domain/ContactRepository.java
public interface ContactRepository {
List<Contact> findAll();
Optional<Contact> findById(Long id);
Contact save(Contact contact);
Contact update(Contact contact);
boolean delete(Long id);
}
¿Por qué una interfaz?
Aquí aparece el primer principio importante de la Arquitectura Hexagonal.
El dominio no debe depender de una implementación concreta.
El dominio solo necesita saber que existe alguien capaz de:
- guardar
- buscar
- actualizar
- eliminar
No le interesa cómo.
Implementar el repositorio en memoria
Ahora sí construiremos la implementación.
Hasta ahora hemos construido el núcleo de la aplicación:
- El modelo de dominio (
Contact). - El puerto de salida (
ContactRepository). - Un adaptador de infraestructura (
InMemoryContactRepository).
Aún no hemos escrito ningún Servlet, lo cual es intencional. Primero consolidamos el dominio para que la lógica de negocio no dependa del protocolo HTTP.
¿Qué ocurre cuando llega una petición HTTP?
Supongamos que un cliente hace esta petición: GET /contact-api/contacts HTTP/1.1
El flujo es el siguiente:
Cliente
│
│ GET /contacts
▼
┌─────────────┐
│ Tomcat │
└──────┬──────┘
│
│ Busca quién atiende /contacts
▼
ContactServlet
│
doGet(request,response)
│
▼
Tu código Java
│
▼
JSON Response
│
▼
Cliente
Observa que Tomcat nunca ejecuta tu método main(), porque en una aplicación Servlet no existe un main().
El contenedor (Tomcat) es quien crea y administra tus Servlets.
¿Qué es un Servlet?
Un Servlet es simplemente una clase Java que sabe responder a peticiones HTTP.
La clase base más utilizada es: HttpServlet
El ciclo de vida de un Servlet
Este es uno de los conceptos más importantes.
Tomcat inicia
│
▼
constructor()
│
▼
init()
│
─────────┼─────────
GET POST PUT DELETE
doGet() doPost() doPut() doDelete()
─────────┼─────────
│
▼
destroy()
Constructor
Java crea el objeto.
Normalmente no escribimos lógica aquí.
init()
Se ejecuta una sola vez.
Aquí es donde normalmente:
- crearías conexiones
- cargarías configuración
- inicializarías repositorios
Ejemplo:
public void init() {
System.out.println("Servlet iniciado");
}
Solo aparece una vez.
doGet()
Cada petición GET ejecuta este método.
GET /contacts
↓
doGet(...)
doPost()
Cada POST.
POST /contacts
↓
doPost(...)
doPut()
Cada PUT.
doDelete()
Cada DELETE.
destroy()
Cuando Tomcat apaga la aplicación.
Solo una vez.
Crear el primer Servlet
public class ContactServlet extends HttpServlet {
@Override
public void init() { ... }
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/plain");
response.getWriter().write("Primer Servlet funcionando");
}
@Override
public void destroy() { ... }
}
¿Cómo sabe Tomcat que existe este Servlet?
Necesita registrarlo.
Hay dos formas:
web.xml(configuración tradicional).- Anotación
@WebServlet(más moderna).
Como estás aprendiendo Servlets desde sus fundamentos, comenzaremos con web.xml. Así entenderás cómo el contenedor descubre y mapea los Servlets antes de introducir anotaciones.
Configurar web.xml
Renemos el archivo https://github.com/codigoelectronica/contact-api/blob/main/src/main/webapp/WEB-INF/web.xml
<servlet>
<servlet-name>ContactServlet</servlet-name>
<servlet-class>
com.example.contacts.adapter.servlet.ContactServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ContactServlet</servlet-name>
<url-pattern>/contacts</url-pattern>
</servlet-mapping>
Cuando llegue una petición: GET /contacts
Tomcat consultará el web.xml: /contacts ↓ ContactServlet ↓ doGet()
El servlet-name es un identificador lógico que conecta la definición del Servlet con el mapeo de la URL. Lo importante es que el nombre usado en <servlet-name> sea exactamente el mismo en ambas secciones.
Implementar GET
Implementar doGet()
Al revisar el repositorio https://github.com/codigoelectronica/contact-api/blob/main/src/main/java/com/example/contacts/adapter/servlet/ContactServlet.java en:
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException { ... }
Implementar POST
Llegamos al conceptos más importantes cuando se trabaja con APIs REST: leer el cuerpo (body) de una petición HTTP.
Hasta ahora solo hemos leído información de la URL. A partir de este punto aprenderás cómo un cliente envía datos al servidor.
HTTP
│
ContactServlet
│
▼
ContactService
│
▼
ContactRepository
│
▼
InMemoryContactRepository
¿Qué ocurre cuando hacemos un POST?
Supongamos que desde Postman enviamos:
POST /contact-api/contacts HTTP/1.1
Content-Type: application/json
{
"name":"Oscar",
"email":"oscar@email.com",
"phone":"300111111"
}
Observa que el JSON no forma parte de la URL.
Viaja dentro del Body.
El flujo es el siguiente:
Cliente
│
JSON en el Body
│
▼
HttpServletRequest
│
▼
request.getReader()
│
▼
ObjectMapper.readValue()
│
▼
Contact
Aquí aparece otro concepto fundamental: El cliente no envía un objeto Java. Envía texto (JSON).
Nuestro trabajo es convertir ese texto en un objeto Java.
¿Cómo leer el Body?
HttpServletRequest proporciona un BufferedReader.
BufferedReader reader = request.getReader();
Podríamos leer el JSON manualmente:
Implementar doPost()
Al revisar el repositorio https://github.com/codigoelectronica/contact-api/blob/main/src/main/java/com/example/contacts/adapter/servlet/ContactServlet.java en:
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws IOException { ... }
Implementar el PUT
Llegamos al método PUT, que suele ser el que más dudas genera porque combina información de dos lugares distintos:
- La URL → identifica qué recurso quieres modificar.
- El Body → contiene los nuevos datos del recurso.
Por ejemplo:
PUT /contact-api/contacts/2
Content-Type: application/json
{
"name":"Ana María",
"email":"ana@email.com",
"phone":"300999999"
}
Nota: Una regla REST de Put importante
El ID del recurso debe venir de la URL, no del JSON. Para evitar esa ambigüedad, ignoraremos el id que venga en el JSON y utilizaremos siempre el de la URL.
Implementar doPut()
Al revisar el repositorio https://github.com/codigoelectronica/contact-api/blob/main/src/main/java/com/example/contacts/adapter/servlet/ContactServlet.java en:
@Override
protected void doPut(HttpServletRequest request,
HttpServletResponse response)
throws IOException { ... }
Revisemos el flujo:
Cliente
│
PUT /contacts/2
│
▼
ContactServlet
│
PathInfo -> /2
│
▼
id = 2
│
▼
readValue(request)
│
▼
Contact
│
▼
contact.setId(2)
│
▼
ContactService.update()
│
▼
Repository
Implementar el DELETE
Para hacer un Delete de un recurso, existen varias opciones válidas:
| Código | Significado | ¿Cuándo usarlo? |
|---|---|---|
| 200 OK | Se eliminó correctamente y devuelves información adicional | Cuando envías un JSON de respuesta |
| 202 Accepted | La eliminación quedó programada | Procesos asíncronos |
| 204 No Content | Se eliminó correctamente y no hay contenido que devolver | La opción más común |
Nosotros utilizaremos: 204 No Content, porque no tiene sentido devolver un objeto que ya no existe.
Implementar el doDelete()
Al revisar el repositorio https://github.com/codigoelectronica/contact-api/blob/main/src/main/java/com/example/contacts/adapter/servlet/ContactServlet.java en:
@Override
protected void doDelete(HttpServletRequest request,
HttpServletResponse response)
throws IOException { ... }
Realizamos el siguiente flujo:
DELETE /contacts/5
│
▼
getId()
│
▼
contactService.delete()
│
▼
Repository.delete()
│
▼
true / false