Cómo crear una API REST con Java Servlets


Aprende paso a paso cómo construir una API REST con Java Servlets, Maven y Arquitectura Hexagonal. Implementa GET, POST, PUT y DELETE sin usar base de datos, comprendiendo el funcionamiento interno de los Servlets.

Cómo crear una API REST con Java Servlets
oscar Escrito por oscar 07 July 2026 4 0

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:

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

arquitectura hexagonal api contactos
Arquitectura hexagonal API contactos

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
Paquetes y relaciones del proyecto
Paquetes y relaciones del proyecto

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:

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:

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:

No le interesa cómo.

Implementar el repositorio en memoria

Ahora sí construiremos la implementación.

https://github.com/codigoelectronica/contact-api/blob/main/src/main/java/com/example/contacts/infrastructure/repository/InMemoryContactRepository.java

Hasta ahora hemos construido el núcleo de la aplicación:

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:

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

Ingresamos a https://github.com/codigoelectronica/contact-api/blob/main/src/main/java/com/example/contacts/adapter/servlet/ContactServlet.java

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:

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:

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

Comentario

Debe aceptar antes de enviar