Definí tus endpoints en un contrato
Dado un Usuario con la siguiente estructura:
{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  },
  "profilePictureUrl": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/1.png"
}
Queremos que nuestro backend exponga estos endpoints:
/userslista todos los usuarios./users/{id}obtiene un usuario por identificador.
OpenAPI specification
Una OpenAPI Specification cumple con la definición de contrato que di antes: Es un conjunto de afirmaciones que contiene valores de entrada válidos y su significado, valores de retorno válidos y su significado, y valores de error que pueden ocurrir y su significado.
Una OpenAPI Specification que representaría el comportamiento deseado se vería algo así:
openapi: 3.0.3
info:
  title: users_manager API
  description: An API for retrieving information from a users database.
  version: 1.0.0
  contact:
    name: Pollito
    url: https://pollito.dev
servers:
  - url: 'http://localhost:8080'
    description: dev
paths:
  /users:
    get:
      tags:
        - User
      summary: List all users
      operationId: findAll
      responses:
        '200':
          description: List of all users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
        default:
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /users/{id}:
    get:
      tags:
        - User
      summary: Get user by identifier
      operationId: findById
      parameters:
        - description: User identifier
          in: path
          name: id
          required: true
          schema:
            format: int64
            type: integer
      responses:
        '200':
          description: A user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        default:
          description: Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    Address:
      description: User address
      properties:
        city:
          description: Address city
          example: "Gwenborough"
          type: string
        geo:
          $ref: '#/components/schemas/Geo'
        street:
          description: Address street
          example: "Kulas Light"
          type: string
        suite:
          description: Address suit
          example: "Apt. 556"
          type: string
        zipcode:
          description: Adress zipcode
          example: "92998-3874"
          type: string
      type: object
    Company:
      description: User company
      properties:
        bs:
          description: Company business
          example: "harness real-time e-markets"
          type: string
        catchPhrase:
          description: Company catch phrase
          example: "Multi-layered client-server neural-net"
          type: string
        name:
          description: Company name
          example: "Romaguera-Crona"
          type: string
      type: object
    Error:
      properties:
        detail:
          description: Description of the problem.
          example: No value present
          type: string
        instance:
          description: The endpoint where the problem was encountered.
          example: "/generate"
          type: string
        status:
          description: http status code
          example: 500
          type: integer
        title:
          description: A short headline of the problem.
          example: "NoSuchElementException"
          type: string
        timestamp:
          description: ISO 8601 Date.
          example: "2024-01-04T15:30:00Z"
          type: string
        trace:
          description: opentelemetry TraceID, a unique identifier.
          example: "0c6a41e22fe6478cc391908406ca9b8d"
          type: string
        type:
          description: used to point the client to documentation where it is explained clearly what happened and why.
          example: "about:blank"
          type: string
      type: object
    Geo:
      description: Address geolocation
      properties:
        lat:
          description: Geolocation latitude
          example: "-37.3159"
          type: string
        lng:
          description: Geolocation longitude
          example: "81.1496"
          type: string
      type: object
    User:
      properties:
        address:
          $ref: '#/components/schemas/Address'
        company:
          $ref: '#/components/schemas/Company'
        email:
          description: User email
          example: "Sincere@april.biz"
          type: string
        id:
          description: User id
          example: 1
          format: int64
          type: integer
        name:
          description: User name
          example: "Leanne Graham"
          type: string
        phone:
          description: User phone
          example: "1-770-736-8031 x56442"
          type: string
        profilePictureUrl:
          description: User profile picture url
          example: "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/1.png"
          type: string
        username:
          description: User username
          example: "Bret"
          type: string
        website:
          description: User website
          example: "hildegard.org"
          type: string
Para una mejor visualización, podés copiar y pegar el archivo YAML en Swagger Editor o usar OpenAPI (Swagger) Editor en IntelliJ IDEA. Deberías poder ver algo así:

Rest y Restful
REST (Representational State Transfer) es un estilo arquitectónico definido por Roy Fielding, que enfatiza:
- Separación cliente-servidor.
 - Ausencia de estado (Statelessness).
 - Capacidad de caché (Cacheability).
 - Sistema en capas (Layered system).
 - Interfaz uniforme (recursos, representaciones).
 - Código bajo demanda (Code-on-demand) (opcional).
 
Las API RESTful siguen estrictamente todas estas restricciones. Pero la realidad es esta: la mayoría de las API "REST" no son verdaderamente RESTful, y eso está bien.
Rest está bien
Las APIs verdaderamente RESTful requieren HATEOAS (Hypermedia as the Engine of Application State) — embeber links a recursos relacionados en las respuestas:
{
  "id": 1,
  "links": [
    { "rel": "self", "href": "/users/1" },
    { "rel": "orders", "href": "/users/1/orders" }
  ]
}
En la práctica, esto no sucede:
- Los equipos de frontend rara vez usan links embebidos.
 - Agrega complejidad sin valor inmediato.
 
Un contrato OpenAPI ya te da el 80% de los beneficios de REST:
- URLs centradas en recursos (
/users/{id}). - Métodos HTTP estándar (
GET/POST/PUT/DELETE). - Manejo claro de errores (códigos 
4xx/5xx). 
Por esas razones, vamos a construir APIs REST. A menos que estés construyendo sistemas impulsados por hipermedia, REST está bien. Enfocate en:
- Contratos consistentes.
 - Manejo predecible de errores.
 - Documentación que humanos y máquinas puedan usar.
 
A medida que tu API evolucione, podés agregar funcionalidades RESTful más adelante si es necesario.
Recurso de aprendizaje recomendado
Te recomiendo mucho el video de High-Performance Programming "Rest API - Best Practices - Design".