Introducción
Contenido
ToggleEn 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:
- Encabezado del Contrato (versión y metadatos)
- Documentación externa y etiquetas
- Paths y Operations: Definición de endpoints
- 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: https://swagger.io/docs/specification/v3_0/basic-structure/
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.

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 123PUT /tickets/123
-> Actualizar el ticket 123DELETE /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:
/tickets
: Esta será la ruta base sobre la que se construirán todas las demás operaciones./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/tickets | Crear un nuevo ticket de soporte. |
GET | /v1/tickets | Obtener 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.
Consulta la documentación: https://swagger.io/docs/specification/v3_0/paths-and-operations/
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

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í:

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:
Campo | Tipo | Descripción |
---|---|---|
title | string | Título corto que resume el problema (mín. 1 - máx. 50). |
description | string | Descripción detallada del problema (mín. 1 - máx. 250). |
reporterId | string (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.
Campo | Tipo | Descripción |
---|---|---|
status | string | Enum: NEW, OPEN, IN_PROGRESS, RESOLVED, CLOSED. Puede omitirse para que el backend asigne NEW por defecto. |
assignedToId | string (uuid) | ID del agente asignado. Puede ser null si aún no se ha asignado nadie. |
priority | string | Enum: LOW, MEDIUM, HIGH, CRITICAL. Valor por defecto: MEDIUM. |
type | string | Enum: 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

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"
}
Campo | Descripción |
---|---|
id | Identificador único del ticket generado por el sistema (UUID). |
title | Título corto proporcionado por el usuario, que resume el problema. |
description | Descripción detallada del ticket. |
status | Estado actual del ticket. Por defecto es NEW, pero puede ser sobrescrito por el cliente. |
reporterId | ID del usuario que creó el ticket. Generalmente se obtiene desde el token JWT cuando hay autenticación. |
assignedToId | ID del agente asignado (puede ser null si aún no se ha asignado a nadie). |
priority | Nivel de urgencia del ticket (LOW, MEDIUM, HIGH, CRITICAL). Tiene valor por defecto. |
type | Tipo de ticket (INCIDENT, SERVICE_REQUEST, QUESTION). Tiene valor por defecto. |
createdAt | Fecha y hora de creación del ticket, en formato ISO 8601. |
updatedAt | Fecha y hora de la última actualización del ticket. Inicialmente es igual a createdAt. |
Endpoint 2: GET /tickets – Listar todos los tickets

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"
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"
}
]
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

/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 encuentraPosible 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

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 pathDevuelve ticket actualizado en la respuesta 200
Endpoint 5: PATCH /tickets/{id} – Cambiar estado del ticket

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

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 pathDevuelve status
204 No Content
si se elimina correctamenteError 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:
- Schemas
- Responses
- Security 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

- CreateTicketRequest: Datos requeridos para crear un ticket (extiende TicketBase).
CreateTicketRequest:
allOf:
- $ref: "#/components/schemas/TicketBase"
- type: object
required:
- title
- description
- reporterId

- UpdateTicketRequest: Datos requeridos para actualizar un ticket completo.
UpdateTicketRequest:
allOf:
- $ref: "#/components/schemas/TicketBase"
- type: object
required:
- title
- description
- status
- reporterId
- priority
- type

- 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]

- 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

- ListTicketResponse: Array de múltiples TicketResponse.
ListTicketResponse:
type: array
items:
$ref: "#/components/schemas/TicketResponse"

- 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

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

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
Link directo del contrato OpenAPI en formato YAML: https://github.com/luisguisadocloud/ticket-system-open-api-contract/blob/main/openapi.yaml