Enfoque API First con OpenApi y Swagger 2: Explicación del ejemplo

Introducción

En la primera parte de este artículo vimos lo que era el enfoque API First y cómo usar OpenAPI para el diseño de contratos claros y bien estructurados desde el inicio.

En esta segunda parte, continuaremos con la explicación detallada de todos los bloques que forman parte de nuestro contrato OpenAPI  el cual está basado en la especificación OpenAPI y documentada con Swagger.

Vamos a partir el contrato OpenAPI en 4 grandes bloques a fin de entender de forma más fácil todo su contenido:

  1. Encabezado del Contrato (versión y metadatos)
  2. Documentación externa y etiquetas
  3. Paths y Operations: Definición de endpoints
  4. Componentes: Piezas reutilizables

Encabezado del Contrato (versión y metadatos)

Comenzamos con la parte inicial del contrato donde se coloca información general del mismo. 

  • openapi: 3.0.4: Es la versión del estándar OpenAPI que se está usando.

  • info: Define los metadatos de la API.

    • title: Nombre de la API.

    • description: Breve descripción funcional.

    • contact: Información de contacto.

    • version: Versión del contrato. Esta versión es manejada por el equipo que crea la API. No confundir con la versión de la especificación OpenAPI.

				
					openapi: 3.0.4
info:
  title: Serverless Ticket System - API
  description: REST API for a serverless ticket system built with AWS Lambda and API Gateway.
  contact:
    email: lguisadom@gmail.com
  version: 1.0.0

				
			

Documentación externa y etiquetas

				
					externalDocs:
  description: My Blog
  url: https://blog.luisguisado.cloud
tags:
  - name: Ticket
    description: Digital record used to track and manage support requests, problems, or incidents.

				
			
tag en swagger

externalDocs: Permite vincular el contrato OpenAPI con alguna documentación externa: wiki, blog, guías de uso, referencias externas, etc.

tags: Es opcional y permite agrupar de forma lógica recursos relacionados. Para nuestra API solo estamos manejando el recurso Tickets. Sin embargo, en sistemas más extensos, podríamos tener múltiples recursos como Usuarios, Órdenes, Productos, etc. 

Paths y operations: definición de Endpoint POST /tickets

Path

Un path es la «ruta» que representa un recurso en una API. En nuestro ejempo el recurso con el que estamos trabajando es ticket. De esta forma, el path /ticket representaría el recurso ticket. Pero, de acuerdo a las buenas prácticas en el diseño de API REST, los paths deben usar sustantivos en plural. Por lo tanto, el path correcto sería /tickets.

Operation

Por otro lado, un operation es una operación o «acción» que se realizará sobre un recurso: crear,  listar, obtener, actualizar, borrar, actualizar parcialmente, etc. 

Cada acción se corresponde con un método HTTP (investigar HTTP request methods).

  • Crear un recurso -> POST
  • Obtener un recurso -> GET
  • Listar todos los recursos -> GET
  • Actualizar un recurso -> PUT
  • Actualizar parcialmente un recurso -> PATCH
  • Eliminar un recurso -> DELETE

Endpoint

Al combinar un operation (acción) con un path (recurso), obtenemos un endpoint, el cual nos la idea de «acción a realizar con el recurso«, lo cual ya representa una funcionalidad más específica. Ejemplos:

  • POST /tickets : Crear un nuevo ticket de soporte.
  • GET /tickets : Obtener una lista de tickets registrados previamente.

Si queremos representar algún recurso en específico, al path se le puede añadir un parámetro de ruta (path param): /tickets/{id}.

Ejemplos:

  • GET /tickets/123 -> Ver el ticket con ID 123
  • PUT /tickets/123 ->  Actualizar el ticket 123
  • DELETE /tickets/123 -> Eliminar el ticket 123

Ahora que ya tenemos más claro los conceptos de path, operation y endpoint y su significado, podemos construir los path base para nuestro contrato:

  1. /tickets: Esta será la ruta base sobre la que se construirán todas las demás operaciones.
  2. /tickets/{id}: Representa un ticket específico.

En total 2 paths.

Al combinar con todas las operaciones necesarias, tendremos como resultado los 6 endpoints para nuestra API. 

  • POST /tickets – Crear un nuevo ticket de soporte.
  • GET /tickets – Obtener la lista de tickets registrados.
  • GET /tickets/{id} – Obtener los detalles de un ticket.
  • PUT /tickets/{id} – Actualizar un ticket existente por completo.
  • PATCH /tickets/{id} – Actualizar un ticket existente de forma parcial.
  • DELETE /tickets/{id} – Eliminar un ticket.

Api versioning

Adicionalmente, como se explicó en el artículo anterior,  adoptaremos desde el inicio  una estrategia de versionamiento de API  (API versioning). Esto nos permitirá evolucionar nuestra API en el futuro sin afectar a los clientes que ya consumen una versión anterior.

Optamos por el enfoque más utilizado: versionado por ruta. Para ello, agregamos el prefijo /v1 en todos los endpoints. Así, la API quedaría de la siguiente manera:

Operation (método)Path (ruta)Descripción
POST/v1/ticketsCrear un nuevo ticket de soporte.
GET/v1/ticketsObtener la lista de tickets registrados.
GET/v1/tickets/{id}Obtener los detalles de un ticket específico.
PUT/v1/tickets/{id}Actualizar los datos de un ticket existente. Requiere enviar todos los campos incluso sin cambios
PATCH/v1/tickets/{id}Actualizar los datos de un ticket existente. Es una actualización parcial.
DELETE/v1/tickets/{id}Eliminar un ticket.

Con esta decisión, en el futuro podríamos tener versiones como /v2/tickets o /v3/tickets conviviendo con la actual, sin romper compatibilidad con clientes existentes.

Campos clave para documentar correctamente un endpoint en OpenAPI

Ya habíamos visto que un endpoint se describe con la combinación de un path (ruta del recurso) y un operation (acción HTTP sobre el recurso). Vamos a ampliar este concepto para especificar una serie de campos o propiedades que permiten documentar de forma más completa los endpoints en OpenAPI.

Cada endpoint tiene una estructura muy similar, pero no todos los campos son obligatorios en todos los casos. Veremos aquí los que más se usan:

summary: Una frase corta que resume la funcionalidad.

				
					summary: Create new ticket
				
			

operationId: Es un identificador único para esa operación dentro del contrato OpenAPI. Documentación: https://swagger.io/docs/specification/v3_0/paths-and-operations/#operationid

				
					operationId: createTicket
				
			

Tags: Permite agrupar los endpoints bajo categorías comunes. En esta API usamos el tag «Ticket» para todos los endpoints relacionados a tickets de soporte.

Documentación: https://swagger.io/docs/specification/v3_0/grouping-operations-with-tags/

				
					tags:
  - Ticket

				
			

security:Define si el endpoint requiere autenticación. Todos nuestros endpoints la requieren, usando JWT con esquema bearerAuth. Documentación: https://swagger.io/docs/specification/v3_0/authentication/bearer-authentication/

				
					security:
  - bearerAuth: []

				
			

Parameters: Cuando el endpoint incluye valores en el path (por ejemplo: /tickets/{id}), se deben declarar como parámetros de tipo path. Documentación: https://swagger.io/docs/specification/v3_0/describing-parameters/

En nuestra API, solo las operaciones GET, PUT, PATCH y DELETE tienen path param {id}.

				
					parameters:
  - in: path
    name: id
    required: true
    schema:
      type: string

				
			

RequestBody: Se utiliza cuando el endpoint espera recibir datos en el cuerpo del request. Aquí definimos el tipo de contenido (application/json) y el esquema a usar (como CreateTicketRequest). Documentación: https://swagger.io/docs/specification/v3_0/describing-request-body/describing-request-body/.

En nuestra API, solo las operaciones POST, PUT y PATCH tienen un body.

				
					
requestBody:
  required: true
  content:
    application/json:
      schema:
        $ref: '#/components/schemas/CreateTicketRequest'
      examples:
        example1:
          summary: Basic ticket
          value:
            title: Bug in production
            description: Users cannot log in
            status: OPEN

				
			

Responses: Cada endpoint debe definir sus posibles respuestas. En el contrato actual se utilizan referencias a componentes comunes (BadRequestError, UnauthorizedError, etc.) y, en algunos casos, esquemas personalizados como TicketResponse o ListTicketResponse.

Documentación: https://swagger.io/docs/specification/v3_0/describing-responses/

				
					
responses:
  '201':
    description: Ticket created
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/TicketResponse'

				
			

Las respuestas que son comunes entre varios endpoints como BadRequest, UnauthorizedError e InternalServerError se pueden referenciar de esta manera:

				
					
"400":
  $ref: "#/components/responses/BadRequestError"
"401":
  $ref: "#/components/responses/UnauthorizedError"
"500":
  $ref: "#/components/responses/InternalServerError"
				
			

Todos los endpoints de esta API utilizan autenticación bearerAuth, trabajan con contenido en formato application/json, y retornan respuestas estándar estructuradas con los códigos HTTP apropiados (200, 201, 400, 404, etc.). Documentación: https://swagger.io/docs/specification/v3_0/using-ref/

Endpoint 1: POST /tickets - Crear un nuevo ticket

Endpoint 1 - Parameters y Request Body
				
					
paths:
  /v1/tickets:
    post:
      tags:
        - Ticket
      security:
        - bearerAuth: []
      summary: Create new ticket
      operationId: createTicket
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTicketRequest"
            examples:
              basicTicket:
                summary: Basic ticket
                value:
                  title: Bug in production
                  description: Users cannot log in to the application
                  status: OPEN
                  reporterId: "550e8400-e29b-41d4-a716-446655440000"
              urgentTicket:
                summary: Urgent ticket
                value:
                  title: Database connection failure
                  description: Critical database connection issue affecting all users
                  status: OPEN
                  reporterId: "550e8400-e29b-41d4-a716-446655440001"
                  priority: CRITICAL
                  type: INCIDENT
              minimalTicket:
                summary: Minimal ticket (using defaults)
                value:
                  title: Login issue
                  description: Users reporting login problems
                  reporterId: "550e8400-e29b-41d4-a716-446655440002"
              customPriorityTicket:
                summary: Ticket with custom priority only
                value:
                  title: Performance issue
                  description: Application is running slow
                  reporterId: "550e8400-e29b-41d4-a716-446655440003"
                  priority: HIGH
              customTypeTicket:
                summary: Ticket with custom type only
                value:
                  title: How to reset password
                  description: Need instructions for password reset
                  reporterId: "550e8400-e29b-41d4-a716-446655440004"
                  type: QUESTION
      responses:
        "201":
          description: Ticket created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketResponse"
              examples:
                basicTicketResponse:
                  summary: Basic ticket response
                  value:
                    id: "1b2c3d4e-5678-90ab-cdef-1234567890ab"
                    title: Bug in production
                    description: Users cannot log in to the application
                    status: OPEN
                    reporterId: "550e8400-e29b-41d4-a716-446655440000"
                    assignedToId: null
                    priority: MEDIUM
                    type: INCIDENT
                    createdAt: "2024-06-27T10:00:00Z"
                    updatedAt: "2024-06-27T10:00:00Z"
                urgentTicketResponse:
                  summary: Urgent ticket response
                  value:
                    id: "2c3d4e5f-6789-01bc-def2-2345678901bc"
                    title: Database connection failure
                    description: Critical database connection issue affecting all users
                    status: OPEN
                    reporterId: "550e8400-e29b-41d4-a716-446655440001"
                    assignedToId: null
                    priority: CRITICAL
                    type: INCIDENT
                    createdAt: "2024-06-27T10:00:00Z"
                    updatedAt: "2024-06-27T10:00:00Z"
                minimalTicketResponse:
                  summary: Minimal ticket response (with defaults)
                  value:
                    id: "3d4e5f6g-7890-12cd-ef34-3456789012cd"
                    title: Login issue
                    description: Users reporting login problems
                    status: OPEN
                    reporterId: "550e8400-e29b-41d4-a716-446655440002"
                    assignedToId: null
                    priority: MEDIUM
                    type: INCIDENT
                    createdAt: "2024-06-27T10:00:00Z"
                    updatedAt: "2024-06-27T10:00:00Z"
        "400":
          $ref: "#/components/responses/BadRequestError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "500":
          $ref: "#/components/responses/InternalServerError"
				
			

Este endpoint permite crear un nuevo ticket de soporte. Representa una operación de tipo POST, lo cual implica que se está creando un recurso dentro del path base /v1/tickets.

  • Método: POST

  • Path: /v1/tickets

  • Operación: Crear recurso

Seguridad: Este endpoint requiere autenticación por bearer token (JWT). Se define en el contrato con:

				
					
      security:
        - bearerAuth: []
				
			

Request Body: La solicitud debe enviar un JSON con los datos del ticket. Es obligatoria (required: true) y sigue la estructura del schema CreateTicketRequest, definido así:

Endpoint 1 - Request Body
				
					
components:
  schemas:
    TicketBase:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 50
          description: Brief title describing the ticket issue
        description:
          type: string
          minLength: 1
          maxLength: 250
          description: Detailed description of the ticket issue
        status:
          type: string
          enum: [NEW, OPEN, IN_PROGRESS, RESOLVED, CLOSED]
          description: Current status of the ticket
        reporterId:
          type: string
          format: uuid
          description: Unique identifier of the user who reported the ticket.
        assignedToId:
          type: string
          format: uuid
          nullable: true
          description: Unique identifier of the user (agent) assigned to resolve the ticket.
        priority:
          type: string
          enum: [LOW, MEDIUM, HIGH, CRITICAL]
          default: MEDIUM
          description: The urgency level of the ticket.
        type:
          type: string
          enum: [INCIDENT, SERVICE_REQUEST, QUESTION]
          default: INCIDENT
          description: The category or type of the request

    CreateTicketRequest:
      allOf:
        - $ref: "#/components/schemas/TicketBase"
        - type: object
          required:
            - title
            - description
            - reporterId
				
			

El schema CreateTicketRequest define los datos que el cliente debe enviar para crear un nuevo ticket. Está definido en base a otro más genérico llamado TicketBase, el cual será un schema reutilizado en endpoints (TicketResponse y UpdateTicketRequest)

Campos requeridos: Estos  son obligatorios para que el backend acepte la creación del ticket:

CampoTipoDescripción
titlestringTítulo corto que resume el problema (mín. 1 - máx. 50).
descriptionstringDescripción detallada del problema (mín. 1 - máx. 250).
reporterIdstring (uuid)Identificador del usuario que reporta el ticket. Este ID puede venir directamente en la petición (fase inicial) o del JWT si se integra un mecanismo de autenticación.

Campos opcionales: Estos campos no son obligatorios al momento de la creación. Además, tienen valores por defecto en caso no se envíen en el cuerpo de la petición.

CampoTipoDescripción
statusstringEnum: NEW, OPEN, IN_PROGRESS, RESOLVED, CLOSED.
Puede omitirse para que el backend asigne NEW por defecto.
assignedToIdstring (uuid)ID del agente asignado. Puede ser null si aún no se ha asignado nadie.
prioritystringEnum: LOW, MEDIUM, HIGH, CRITICAL. Valor por defecto: MEDIUM.
typestringEnum: INCIDENT, SERVICE_REQUEST, QUESTION. Valor por defecto: INCIDENT.
				
					{
  "title": "Bug en producción",
  "description": "Los usuarios no pueden iniciar sesión",
  "status": "OPEN",          // optional
  "priority": "CRITICAL",    // optional
  "type": "INCIDENT",        // optional
  "reporterId": "63c1b730-f45d-4e3e-9392-8392b578aa0a" // required
}

				
			

Ejemplos incluidos en el contrato: El contrato incluye ejemplos predefinidos, lo cual es muy útil para frontend o testing:

				
					
            examples:
              basicTicket:
                summary: Basic ticket
                value:
                  title: Bug in production
                  description: Users cannot log in to the application
                  status: OPEN
                  reporterId: "550e8400-e29b-41d4-a716-446655440000"
              urgentTicket:
                summary: Urgent ticket
                value:
                  title: Database connection failure
                  description: Critical database connection issue affecting all users
                  status: OPEN
                  reporterId: "550e8400-e29b-41d4-a716-446655440001"
                  priority: CRITICAL
                  type: INCIDENT
              minimalTicket:
                summary: Minimal ticket (using defaults)
                value:
                  title: Login issue
                  description: Users reporting login problems
                  reporterId: "550e8400-e29b-41d4-a716-446655440002"
              customPriorityTicket:
                summary: Ticket with custom priority only
                value:
                  title: Performance issue
                  description: Application is running slow
                  reporterId: "550e8400-e29b-41d4-a716-446655440003"
                  priority: HIGH
              customTypeTicket:
                summary: Ticket with custom type only
                value:
                  title: How to reset password
                  description: Need instructions for password reset
                  reporterId: "550e8400-e29b-41d4-a716-446655440004"
                  type: QUESTION
				
			

Respuestas posibles:

  • 201 TicketResponse: Devuelve el ticket creado con sus atributos completos (incluyendo ID, timestamps)
  • 400 BadRequestError: Faltan campos o no cumplen con el esquema
  • 401 UnauthorizedError: No se incluyó o es inválido el token JWT
  • 500 Internal Server Error: Error inesperado en el backend
endpoint1-3 responses
				
					
responses:
    "201":
      description: Ticket created
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/TicketResponse"
          examples:
            basicTicketResponse:
              summary: Basic ticket response
              value:
                id: "1b2c3d4e-5678-90ab-cdef-1234567890ab"
                title: Bug in production
                description: Users cannot log in to the application
                status: OPEN
                reporterId: "550e8400-e29b-41d4-a716-446655440000"
                assignedToId: null
                priority: MEDIUM
                type: INCIDENT
                createdAt: "2024-06-27T10:00:00Z"
                updatedAt: "2024-06-27T10:00:00Z"
            urgentTicketResponse:
              summary: Urgent ticket response
              value:
                id: "2c3d4e5f-6789-01bc-def2-2345678901bc"
                title: Database connection failure
                description: Critical database connection issue affecting all users
                status: OPEN
                reporterId: "550e8400-e29b-41d4-a716-446655440001"
                assignedToId: null
                priority: CRITICAL
                type: INCIDENT
                createdAt: "2024-06-27T10:00:00Z"
                updatedAt: "2024-06-27T10:00:00Z"
            minimalTicketResponse:
              summary: Minimal ticket response (with defaults)
              value:
                id: "3d4e5f6g-7890-12cd-ef34-3456789012cd"
                title: Login issue
                description: Users reporting login problems
                status: OPEN
                reporterId: "550e8400-e29b-41d4-a716-446655440002"
                assignedToId: null
                priority: MEDIUM
                type: INCIDENT
                createdAt: "2024-06-27T10:00:00Z"
                updatedAt: "2024-06-27T10:00:00Z"
    "400":
      $ref: "#/components/responses/BadRequestError"
    "401":
      $ref: "#/components/responses/UnauthorizedError"
    "500":
      $ref: "#/components/responses/InternalServerError"
				
			

Ejemplo de respuesta 201:

Cuando se crea un nuevo ticket mediante el endpoint POST /v1/tickets, el servidor devuelve una respuesta con el código HTTP 201 (Created) y un cuerpo en formato JSON que representa el objeto TicketResponse creado. Este objeto contiene todos los campos del ticket, tanto los enviados por el cliente como los generados automáticamente por el backend:

				
					{
  "id": "1b2c3d4e-5678-90ab-cdef-1234567890ab",
  "title": "Bug in production",
  "description": "Users cannot log in to the application",
  "status": "OPEN",
  "reporterId": "550e8400-e29b-41d4-a716-446655440000",
  "assignedToId": null,
  "priority": "MEDIUM",
  "type": "INCIDENT",
  "createdAt": "2024-06-27T10:00:00Z",
  "updatedAt": "2024-06-27T10:00:00Z"
}
				
			
CampoDescripción
idIdentificador único del ticket generado por el sistema (UUID).
titleTítulo corto proporcionado por el usuario, que resume el problema.
descriptionDescripción detallada del ticket.
statusEstado actual del ticket. Por defecto es NEW, pero puede ser sobrescrito por el cliente.
reporterIdID del usuario que creó el ticket. Generalmente se obtiene desde el token JWT cuando hay autenticación.
assignedToIdID del agente asignado (puede ser null si aún no se ha asignado a nadie).
priorityNivel de urgencia del ticket (LOW, MEDIUM, HIGH, CRITICAL). Tiene valor por defecto.
typeTipo de ticket (INCIDENT, SERVICE_REQUEST, QUESTION). Tiene valor por defecto.
createdAtFecha y hora de creación del ticket, en formato ISO 8601.
updatedAtFecha y hora de la última actualización del ticket. Inicialmente es igual a createdAt.

Endpoint 2: GET /tickets – Listar todos los tickets

endpoint 2 - responses
				
					
    get:
      tags:
        - Ticket
      security:
        - bearerAuth: []
      summary: Get all tickets
      operationId: listTickets
      responses:
        "200":
          description: List of tickets
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListTicketResponse"
              examples:
                ticketsList:
                  summary: List of tickets
                  value:
                    - id: "1b2c3d4e-5678-90ab-cdef-1234567890ab"
                      title: Bug in production
                      description: Users cannot log in to the application
                      status: OPEN
                      reporterId: "550e8400-e29b-41d4-a716-446655440000"
                      assignedToId: null
                      priority: MEDIUM
                      type: INCIDENT
                      createdAt: "2024-06-27T10:00:00Z"
                      updatedAt: "2024-06-27T10:00:00Z"
                    - id: "2c3d4e5f-6789-01bc-def2-2345678901bc"
                      title: Database connection failure
                      description: Critical database connection issue affecting all users
                      status: OPEN
                      reporterId: "550e8400-e29b-41d4-a716-446655440001"
                      assignedToId: null
                      priority: CRITICAL
                      type: INCIDENT
                      createdAt: "2024-06-27T10:00:00Z"
                      updatedAt: "2024-06-27T10:00:00Z"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "500":
          $ref: "#/components/responses/InternalServerError"
				
			
Este endpoint es típicamente usado en el dashboard principal de la aplicación para mostrar todos los tickets.
  • Método: GET

  • No requiere requestBody

  • Respuesta exitosa: HTTP 200 OK con un array de tickets (ListTicketResponse)

Respuesta esperada

				
					[
  {
    "id": "1b2c3d4e-5678-90ab-cdef-1234567890ab",
    "title": "Bug in production",
    "description": "Users cannot log in to the application",
    "status": "OPEN",
    "reporterId": "550e8400-e29b-41d4-a716-446655440000",
    "assignedToId": null,
    "priority": "MEDIUM",
    "type": "INCIDENT",
    "createdAt": "2024-06-27T10:00:00Z",
    "updatedAt": "2024-06-27T10:00:00Z"
  },
  {
    "id": "2c3d4e5f-6789-01bc-def2-2345678901bc",
    "title": "Database connection failure",
    "description": "Critical database connection issue affecting all users",
    "status": "OPEN",
    "reporterId": "550e8400-e29b-41d4-a716-446655440001",
    "assignedToId": null,
    "priority": "CRITICAL",
    "type": "INCIDENT",
    "createdAt": "2024-06-27T10:00:00Z",
    "updatedAt": "2024-06-27T10:00:00Z"
  }
]
				
			
💡 Nota:

En los endpoints de tipo GET que listan recursos (como GET /tickets), es común usar filtros de búsqueda, ordenamiento o paginación utilizando query parameters.

Por ejemplo: /tickets?status=OPEN&sort=createdAt&limit=10&page=2

Esto no está incluido en nuestro ejemplo para mantenerlo simple, pero si deseas profundizar en cómo diseñar correctamente estos filtros, puedes revisar la documentación: Query Params

Endpoint 3: GET /tickets/{id} – Obtener un ticket específico

endpoint 3 - parameters y responses
				
					
  /v1/tickets/{id}:
    get:
      tags:
        - Ticket
      security:
        - bearerAuth: []
      summary: Get ticket by ID
      operationId: getTicket
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the ticket
      responses:
        "200":
          description: Ticket found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketResponse"
        "400":
          $ref: "#/components/responses/BadRequestError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "500":
          $ref: "#/components/responses/InternalServerError"
				
			

Este endpoint se usa para buscar un recurso por su identificador, y poder devolver su detalle.

Incluye parámetro de ruta id:

  • Devuelve un solo objeto TicketResponse si se encuentra

  • Posible error 404 si el ticket no existe

				
					
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the ticket
				
			

Endpoint 4: PUT /tickets/{id} – Actualizar un ticket completo

endpoint 4 - parameters - request y responses

El PUT es útil cuando se desea sobrescribir todos los campos del recurso, a diferencia del PATCH que es parcial.

				
					
    put:
      tags:
        - Ticket
      security:
        - bearerAuth: []
      summary: Update ticket by ID (Total replacement of the entire resource)
      operationId: updateTicket
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the ticket
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTicketRequest"
      responses:
        "200":
          description: Ticket updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TicketResponse"
        "400":
          $ref: "#/components/responses/BadRequestError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "500":
          $ref: "#/components/responses/InternalServerError"
				
			
  • Método: PUT (actualización completa del recurso)

  • Requiere requestBody con todos los campos requeridos (title, description, status)

  • Requiere id en el path

  • Devuelve ticket actualizado en la respuesta 200

Endpoint 5: PATCH /tickets/{id} – Cambiar estado del ticket

endpoint 5 - parameters, request body y responses

La operación PATCH sirve para escenarios donde queremos actualizar solo uno o varios campos, es decir, actualizaciones parciales y flexibles. Si no se envía ningún campo, el sistema debería responder con un error.

				
					    patch:
      tags:
        - Ticket
      security:
        - bearerAuth: []
      summary: Update ticket by ID (Partial update of the resource)
      operationId: patchTicket
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the ticket
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchTicketRequest"
      responses:
        "204":
          description: Ticket updated successfully
        "400":
          $ref: "#/components/responses/BadRequestError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "500":
          $ref: "#/components/responses/InternalServerError"
				
			
  • Path: /tickets/{id}

  • Método: PATCH (modificación parcial)

  • Request body: con todos los campos opcionales (title, description, status). Pero se debe enviar al menos 1.

				
					
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PatchTicketRequest"
				
			
  • Respuesta 204 si se actualiza correctamente

  • Error 400 si el estado es inválido

Endpoint 6: DELETE /tickets/{id} – Eliminar un ticket

endpoint 6 - parameters, responses

El DELETE se usa para eliminar un recurso.

				
					
    delete:
      tags:
        - Ticket
      security:
        - bearerAuth: []
      summary: Delete ticket by ID
      operationId: deleteTicket
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            format: uuid
          description: Unique identifier of the ticket
      responses:
        "204":
          description: Ticket deleted
        "400":
          $ref: "#/components/responses/BadRequestError"
        "404":
          $ref: "#/components/responses/NotFoundError"
        "401":
          $ref: "#/components/responses/UnauthorizedError"
        "500":
          $ref: "#/components/responses/InternalServerError"
				
			
  • No requiere requestBody

  • Requiere solo id en el path

  • Devuelve status 204 No Content si se elimina correctamente

  • Error 404 si el ticket no existe

Componentes: Piezas reutilizables

En OpenAPI, la sección components es una especie biblioteca de piezas reutilizables: esquemas, respuestas, autenticaciones, encabezados, parámetros, etc. Esto permite mantener la API modular, DRY (Don’t Repeat Yourself) y más fácil de mantener.

Documentación: https://swagger.io/docs/specification/v3_0/components/

En el contrato actual se usan 3 tipos de componentes principales:

  1. Schemas
  2. Responses
  3. Security Schemas

Schemas

schemas
schemas

Los schemas son los objetos que se usan en los requestBody y en las respuestas de los endpoints. Se representan con la notación components.schemas.

  • TicketBase: Estructura común con title, description, status.
				
					
  schemas:
    TicketBase:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 50
          description: Brief title describing the ticket issue
        description:
          type: string
          minLength: 1
          maxLength: 250
          description: Detailed description of the ticket issue
        status:
          type: string
          enum: [NEW, OPEN, IN_PROGRESS, RESOLVED, CLOSED]
          description: Current status of the ticket
        reporterId:
          type: string
          format: uuid
          description: Unique identifier of the user who reported the ticket.
        assignedToId:
          type: string
          format: uuid
          nullable: true
          description: Unique identifier of the user (agent) assigned to resolve the ticket.
        priority:
          type: string
          enum: [LOW, MEDIUM, HIGH, CRITICAL]
          default: MEDIUM
          description: The urgency level of the ticket.
        type:
          type: string
          enum: [INCIDENT, SERVICE_REQUEST, QUESTION]
          default: INCIDENT
          description: The category or type of the request
				
			
Schema TicketBase
Schema TicketBase
  • CreateTicketRequest: Datos requeridos para crear un ticket (extiende TicketBase).
				
					
    CreateTicketRequest:
      allOf:
        - $ref: "#/components/schemas/TicketBase"
        - type: object
          required:
            - title
            - description
            - reporterId
				
			
Schema CreateTicketRequest
Schema CreateTicketRequest
  • UpdateTicketRequest: Datos requeridos para actualizar un ticket completo.
				
					
    UpdateTicketRequest:
      allOf:
        - $ref: "#/components/schemas/TicketBase"
        - type: object
          required:
            - title
            - description
            - status
            - reporterId
            - priority
            - type

				
			
Schema UpdateTicketRequest
Schema UpdateTicketRequest
  • PatchTicketRequest:  Datos requeridos para actualizar un ticket de forma parcial.
				
					
    PatchTicketRequest:
      type: object
      minProperties: 1
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 50
        description:
          type: string
          minLength: 1
          maxLength: 250
        status:
          type: string
          enum: [NEW, OPEN, IN_PROGRESS, RESOLVED, CLOSED]
        assignedToId:
          type: string
          format: uuid
          nullable: true
        priority:
          type: string
          enum: [LOW, MEDIUM, HIGH, CRITICAL]
        type:
          type: string
          enum: [INCIDENT, SERVICE_REQUEST, QUESTION]
				
			
Schema PatchTicketRequest
Schema PatchTicketRequest
  • TicketResponse:  Representación de un ticket completo (con id, timestamps).
				
					
    TicketResponse:
      allOf:
        - $ref: "#/components/schemas/TicketBase"
        - type: object
          required:
            - id
            - title
            - description
            - status
            - reporterId
            - priority
            - type
            - createdAt
            - updatedAt
          properties:
            id:
              type: string
              format: uuid
              description: Unique identifier of the ticket
            createdAt:
              type: string
              format: date-time
              description: Timestamp when the ticket was created
            updatedAt:
              type: string
              format: date-time
              description: Timestamp when the ticket was last updated
				
			
Schema TicketResponse
Schema TicketResponse
  • ListTicketResponse: Array de múltiples TicketResponse.
				
					
    ListTicketResponse:
      type: array
      items:
        $ref: "#/components/schemas/TicketResponse"
				
			
Schema ListTicketResponse
Schema ListTicketResponse
  • ErrorResponse: Estructura estándar para errores.
				
					
    ErrorResponse:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Error code identifier
        message:
          type: string
          description: Human-readable error message
        details:
          type: array
          items:
            type: string
          nullable: true
          description: Additional error details
				
			
Schema ErrorResponse
Schema ErrorResponse

Responses

Aquí definimos las respuestas de nuestros endpoints pero de una forma reutilizable:

  • BadRequestError: 400 – Validación fallida
  • UnauthorizedError: 401 – Falta token o token inválido
  • NotFoundError: 404 – Recurso no encontrado
  • InternalServerError: 500 – Error inesperado del servidor

 A su vez, como estas respuestas tienen la misma estructura, hacemos referencia al schema ErrorResponse que ya fue definido en la sección schema y que contiene la estructura compartida por todas las respuestas de error.

Documentación: https://swagger.io/docs/specification/v3_0/describing-responses/#reusing-responses

				
					
responses:
    UnauthorizedError:
      description: Missing or invalid token
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            code: unauthorized
            message: Missing bearer token

    NotFoundError:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            code: ticket_not_found
            message: Ticket not found

    BadRequestError:
      description: Invalid input
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            code: bad_request
            message: Validation failed
            details:
             - "Title is required and must be between 1 and 50 characters"
             - "Description is required and must be between 1 and 250 characters"
             - "Reporter ID must be a valid UUID format"

    InternalServerError:
      description: Unexpected internal error
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            code: internal_server_error
            message: Unexpected server error
				
			
responses

Security Schemas

Por último, en esta sección se define el mecanismo de autenticación para nuestra API. En este ejemplo estoy considerando que se usará autenticación basada  en tokens, conocida también como Token Authentication o Bearer Authentication (RFC 6750).

En OpenAPI 3.0, la autenticación basada en tokens se define como un security scheme con type: http y scheme: bearer. De manera opcional se especifica bearerFormat: JWT para propósitos de documentación.

				
					
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
				
			

Luego, podemos aplicar este security scheme a nivel global de toda la API o de forma individual en cada endpoint. En este ejemplo, lo he aplicado individualmente en cada una de las operaciones de la siguiente manera:

				
					
security:
  - bearerAuth: []

				
			

Aparte de Bearer Authentication, OpenAPI soporta otros tipos de esquemas de autenticación:

Aquí termina la parte 2. Con este artículo hemos terminado el recorrido por el diseño de un contrato OpenAPI funcional. Considero que puedes usar este contrato como punto de partida para seguir profundizando y escribir tus propios contratos y generar documentación para posibles clientes o proyectos propios.

Tu feedback me ayuda a seguir mejorando y aprendiendo. Si te resultó útil cuéntame qué te pareció y qué temas te gustaría ver en las próximas publicaciones.

Link del repositorio del Contrato OpenAPI: https://github.com/luisguisadocloud/ticket-system-open-api-contract

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio