Validación de Api Gateway con OpenAPI
Tags: aws, openapi, apigateway, lambda
Published: 2023-01-16 05:23:28

La validación de AWS API Gateway con OpenAPI es una práctica recomendada para garantizar que las peticiones y respuestas cumplen con los estándares esperados. OpenAPI, también conocido como Swagger, es un estándar abierto para describir y documentar APIs. Al utilizar OpenAPI en conjunto con AWS API Gateway, se pueden validar automáticamente las peticiones y respuestas y garantizar que solo se procesan las que cumplen con los estándares esperados.

## OpenAPI

Es un estándar para describir la estructura de una API, incluidos los endpoints (puntos finales URL), los formatos de solicitud y respuesta y las operaciones HTTP (GET, POST, PUT, etc) que se implementa en una API generalmente. Este documento también llamado Swagger utiliza el formato JSON o YAML para definir una API. Para más información revisar: [OpenAPI](https://swagger.io/docs/specification/about/)

## API Gateway y OpenAPI

El servicio API Gateway de AWS es compatible con la especificación OpenAPI, esto permite estructurar información de la API, puntos finales, autorizadores (token y api-keys), esquemas y validar entradas que se realizan.

La validación de las peticiones en AWS API Gateway se puede realizar utilizando los esquemas definidos en el archivo OpenAPI. Los esquemas especifican el formato esperado de las peticiones, incluyendo los parámetros de la URL, los encabezados y el cuerpo de la petición. Al utilizar estos esquemas, se pueden validar automáticamente las peticiones para garantizar que cumplen con los estándares esperados.

**Funcionamiento API Gateway**

Todo empieza cuando un cliente realiza una petición (GET para este ejemplo) hacia nuestra Api Gateway. Api Gateway lo que hace es crear un objeto json típico de una petición HTTP, y mediante una integración que se especifica en la plantilla OpenApi, realiza un POST a un servicio de AWS (una lambda para este caso). Esto ejecuta la lambda y realiza la transacción, para el retorno de los datos se debe modelar como una respuesta HTTP válida, esto para que lo transforme Api Gateway y pueda retornar la solicitud HTTP de manera exitosa. Mediante la especificación de Open API la idea es realizar una validación a nivel de ese Api Gateway y no en la lambda.

![Image description API](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gphwhp1yh5ttwrs7fuoq.jpg)

## Escenario a probar

Para este caso se valida algunos endpoints implementados en API Gateway, estos endpoints mandan a llamar una función lambda. La configuración de la plantilla del stack y de open api es independiente del lenguaje que se use, para este caso se usa Java.

**Código Actual**

Se muestra las funciones lambdas en Java para esta prueba:

Función para recuperar todos los clientes:
``` java
package com.kc.cloud.labs.aws.lambda.customers;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.kc.cloud.labs.aws.lambda.customers.models.Customer;
import com.kc.cloud.labs.aws.lambda.customers.services.CustomerService;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;

public class LabsCustomersGETAll implements RequestHandler
        <APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

    private static final Logger logger = Logger.getLogger(LabsCustomersGETAll.class.getName());

    @Override
    public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
        APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
        logger.info("LabsCustomersGETAll.handleRequest() invoked");
        List<Customer> customers = CustomerService.getAllCustomers();
        response.setHeaders(getHeaders());
        response.setBody(getBody(customers));
        response.setStatusCode(200);
        return response;
    }

    public String getBody(List<Customer> customers){
        StringBuilder customersJson = new StringBuilder("[");
        for (Customer customer : customers) {
            customersJson.append(customer.toJSON()).append(",");
        }
        customersJson.deleteCharAt(customersJson.length() - 1);
        customersJson.append("]");
        return customersJson.toString();
    }

    public HashMap<String, String> getHeaders() {
        HashMap<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/json");
        headers.put("X-Custom-Header", "application/json");
        return headers;
    }
}

```

Función para recuperar un Cliente por id:
``` java
package com.kc.cloud.labs.aws.lambda.customers;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.kc.cloud.labs.aws.lambda.customers.models.Customer;
import com.kc.cloud.labs.aws.lambda.customers.services.CustomerService;

import java.util.HashMap;
import java.util.logging.Logger;

public class LabsCustomersGETById implements RequestHandler
        <APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

    private static final Logger logger = Logger.getLogger(LabsCustomersGETById.class.getName());

    @Override
    public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {

        logger.info("LabsCustomersGETById.handleRequest() invoked");

        String id = input.getPathParameters().get("id");
        Customer customer = CustomerService.getCustomerById(Integer.parseInt(id));

        if (customer == null) {
            return new APIGatewayProxyResponseEvent()
                    .withStatusCode(404)
                    .withBody("{\"message\": \"Customer not found\"}");
        }

        return new APIGatewayProxyResponseEvent()
                .withStatusCode(200)
                .withHeaders(getHeaders())
                .withBody(customer.toJSON());
    }

    public HashMap<String, String> getHeaders() {
        HashMap<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/json");
        headers.put("X-Custom-Header", "application/json");
        return headers;
    }

}

```

Función para crear un Cliente
``` java
package com.kc.cloud.labs.aws.lambda.customers;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.kc.cloud.labs.aws.lambda.customers.models.Customer;
import com.kc.cloud.labs.aws.lambda.customers.services.CustomerService;

import java.util.HashMap;
import java.util.logging.Logger;


public class LabsCustomersPSTCreate implements RequestHandler
        <APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> {

    private static final Logger logger = Logger.getLogger(LabsCustomersPSTCreate.class.getName());


    @Override
    public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
        logger.info("LabsCustomersPSTCreate.handleRequest() invoked");
        String body = input.getBody();
        Customer customerCreated = CustomerService.createCustomer(body);
        return new APIGatewayProxyResponseEvent()
                .withStatusCode(201)
                .withHeaders(getHeaders())
                .withBody("{\"message\": \"Customer created\", \"customerId\": \"" + customerCreated.getId() + "\"}");
    }

    public HashMap<String, String> getHeaders() {
        HashMap<String, String> headers = new HashMap<>();
        headers.put("Content-Type", "application/json");
        headers.put("X-Custom-Header", "application/json");
        return headers;
    }
}

```

**Plantilla para crear el stack SAM yaml**

Se tiene una API con tres endpoints una para listar todos los clientes, obtener un cliente y crear un cliente, la idea es validar las solicitudes que se realizan utilizando la especificación Open API que se está hablando.

Se muestra la plantilla SAM para desplegar el stack:


``` yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  kc-labs-serverless
  Sample SAM Template 

Globals:
  Function:
    CodeUri: kc-labs-app
    Timeout: 20
    Tracing: Active
    Runtime: java11
    MemorySize: 512
    Architectures:
      - x86_64
  Api:
    TracingEnabled: True

Resources:
  ApiGatewayApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: v1
      DefinitionBody:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: ./specs/open_api.yaml
    
  LabsCustomersGETAllLambda:
    Type: AWS::Serverless::Function
    Properties:
      Handler: com.kc.cloud.labs.aws.lambda.customers.LabsCustomersGETAll::handleRequest
      FunctionName: !Sub ${AWS::StackName}-LabsCustomersGETAll
      Environment:
        Variables:
          PARAM1: VALUE
          JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
      Events:
        EventApiLabsCustomersGETAll:
          Type: Api
          Properties:
            Path: /labs/customers
            Method: get
            RestApiId: !Ref ApiGatewayApi

  LabsCustomersGETByIdLambda:
    Type: AWS::Serverless::Function
    Properties:
      Handler: com.kc.cloud.labs.aws.lambda.customers.LabsCustomersGETById::handleRequest
      FunctionName: !Sub ${AWS::StackName}-LabsCustomersGETById
      Environment:
        Variables:
          PARAM1: VALUE
          JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
      Events:
        EventApiLabsCustomersGETById:
          Type: Api
          Properties:
            Path: /labs/customers/{id}
            Method: get
            RestApiId: !Ref ApiGatewayApi

  LabsCustomersPSTCreateLambda:
    Type: AWS::Serverless::Function
    Properties:
      Handler: com.kc.cloud.labs.aws.lambda.customers.LabsCustomersPSTCreate::handleRequest
      FunctionName: !Sub ${AWS::StackName}-LabsCustomersPSTCreate
      Environment:
        Variables:
          PARAM1: VALUE
          JAVA_TOOL_OPTIONS: -XX:+TieredCompilation -XX:TieredStopAtLevel=1
      Events:
        EventApiLabsCustomersPSTCreate:
          Type: Api
          Properties:
            Path: /labs/customers
            Method: post
            RestApiId: !Ref ApiGatewayApi
```
Tomar en cuenta que se crea el recurso para desplegar una Api en AWS, y se especifica la plantilla de Open API para que pueda tomar esa configuración al momento de realizar el despliegue:

![Image description 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ixgajzj5v0r2egcv5jfe.png)

> Nota: Toda esa implementación de OpenAPI puede ir en la misma plantilla SAM, pero es conveniente separarlo, ya que generalmente la plantilla de OpenAPI es grande.

Se muestra a continuación la plantilla OpenAPI utilizada.

``` yaml
openapi: 3.0.1
info:
  title: 
    Fn::Sub: Labs Validate Request - ${AWS::StackName} 
  version: 1.0.0
  description: |
    This API validates the request body and returns the request body as a response.
    This API is used for testing the API Gateway Labs.

x-amazon-apigateway-request-validators:
  all:
    validateRequestBody: true
    validateRequestParameters: true
  params-only:
    validateRequestBody: false
    validateRequestParameters: true
  
paths: 
  /labs/customers:
    get:
      summary: Get all customers
      description: Get all customers
      operationId: LabsCustomersGETAllLambda
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Customer'
      x-amazon-apigateway-integration:
        type: aws_proxy
        timeoutInMillis: 20000
        httpMethod: POST
        uri:
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LabsCustomersGETAllLambda.Arn}/invocations
        responses:
          default:
            statusCode: 200
  
    post:
      summary: Get a customer by ID
      description: Get a customer by ID
      operationId: LabsCustomersPSTCreateLambda
      x-amazon-apigateway-request-validator: all
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Customer'
    

      responses:
        201:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
      x-amazon-apigateway-integration:
        type: aws_proxy
        timeoutInMillis: 20000
        httpMethod: POST
        uri:
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LabsCustomersPSTCreateLambda.Arn}/invocations
        responses:
          default:
            statusCode: 201


  /labs/customers/{id}:
    get:
      summary: Get a customer by ID
      description: Get a customer by ID
      operationId: LabsCustomersGETByIdLambda
      x-amazon-apigateway-request-validator: params-only
      parameters:
        - name: id
          in: path
          description: Customer ID
          required: true
          schema:
            type: integer
            minimum: 1
        - name: custom
          in: header
          description: custom
          required: true
          schema:
            type: boolean
        - name: page
          in: query
          description: Page number
          required: true
          schema:
            type: integer
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
      x-amazon-apigateway-integration:
        type: aws_proxy
        timeoutInMillis: 20000
        httpMethod: POST
        uri:
          Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LabsCustomersGETByIdLambda.Arn}/invocations
        responses:
          default:
            statusCode: 200

components:
  schemas:
    Customer:
      type: object
      properties:
        id:
          type: integer
        fullName:
          type: string
        isPremium:
          type: boolean
      required:
        - id
        - fullName
        - isPremium
```

> Nota: En las plantillas OpenAPI también podemos usar las funciones intrínsecas para inyectar valores de los recursos o del mismo stack.

A continuación se detalla cada una de las partes de esta plantilla mostrada anteriormente:

**Información general de la API**

Se detalla la información general de la API que se está implementado como el título, la descripción y la versión:

![Image description 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hv3xbrd5zgm0iwdtrg6p.png)

**Exensiones para validar las solicitudes**

AWS ofrece algunas extensiones que se usan con el estándar OpenAPI y para validar la data entrante de usa x-amazon-apigateway-request-validators, para más información consultar [OpenAPI extensiones](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions.html):

- all: Indica que la validación lo hace en el cuerpo de la solicitud y en los parámetros (headers, paths y queries)
- params-only: Solo válida los parámetros

![Image description 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/giaef5of8wccuf7cagp2.png)

**Configuración de los endpoints**

En esta parte se define los puntos finales, el tipo de solicitud, información general y mediante la extensión x-amazon-apigateway-integration se puede anexar la función lambda a utilizar.

Obtener todos los clientes:

Para este caso no se realiza ninguna validación en la solicitud, simplemente se configura información general del endpoint, las respuestas esperadas y la integración con el servicio de AWS.

![Image description 4](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v2frhjp6thkv8ycgb3jd.png)

> Nota: La parte del esquema $ref: '#/components/schemas/Customer', hace referencia a un modelo de dato que se lo define en el mismo documento OpenAPI por lo general aunque se puede tener por archivos separados.

Crear un cliente: 

Para este caso se valida el cuerpo de la solicitud y los parámetros.

![Image description 5](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9oe3a53cbww9l6ses139.png)

Obtener un cliente por ID:

Para este caso se valida los parámetros de la solicitud, en este caso se tiene path, header y una query que todas son obligatorias.

![Image description 6](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rad9kvkxgds1faf9s0c1.png)

**Componentes**

Los componentes son una manera de modelar datos y ser referenciadas para validar entrada de datos, tomar en cuenta que en este caso todos los campos son obligatorios.


![Image description 7](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3n8dgtfivrusd9ipf749.png)

**Despliegue de Stack**

Una vez realizadas esas configuraciones en las plantillas y creadas las funciones se puede desplegar el stack usando SAM.

`sam build`
`sam deploy`
Si es un nuevo stack:
`sam deploy --guided`

## Probando funcionalidad

Después podemos probar los endpoints y ver si la especificación de OpenAPI realizada esta funcionando de manera adecuada:

Obtener todos los clientes:

Recordar que para el endpoint de obtener todos los clientes no se configuró ninguna validación, por lo que funciona sin problemas.

![Image description 8](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/atgbyenoq1pb1df8mw1x.png)

Obtener un cliente por ID:

Para este caso recordar que se configuró la validación a nivel de parámetros, en este caso para los tres tipos, en headers, paths y queries.

Si se enviá todas esas configuraciones la ejecución de la API lo realiza sin problemas.

![Image description 9](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7nl1wu9izppy8dnz081x.png)

Pero si faltan algunos de los puntos que son requeridos la petición es rechazada:

![Image description 10](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ks7ccviov27f0o765tsx.png)

Esto quiere decir que la falla fue a nivel de la API Gateway y la lambda nunca ha sido llamada, eso se lo puede comprobar en X-RAY: 

![Image description 11](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uwaxz0ve5fl794maxhju.png)

Crear un cliente:

Para este caso la validación era en ambos casos, sin embargo solo se envía un cuerpo en la solicitud, que es la información del cliente, se envía un body válido y se ejecuta sin problemas:

![Image description 12](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d47au9z8h5aq7dzljvca.png)

Si se envía un campo con un tipo de dato que no está registrado en el componente Customer, la ejecución no lo realiza correctamente:

![Image description 13](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sw4ji72uncbv6vygntf1.png)

## **Conclusiones**
- AWS API Gateway con OpenAPI es una excelente manera de garantizar que las peticiones y respuestas cumplen con los estándares esperados y de documentar de forma automatizada la API. 
- Al utilizar OpenAPI en conjunto con AWS API Gateway, se pueden validar automáticamente las peticiones y respuestas, lo que ayuda a aumentar la confianza en la API y a disminuir los errores.
- No todas las propiedades del esquema OpenAPI son compatibles, en este caso la validación lo realiza solo verificando si el campo requerido efectivamente viene en la solicitud, sin embargo otras propiedades como el maximum o minimum solo se coloca por temas de documentación.

## Referencias.
- https://swagger.io/docs/specification/about/
- https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions.html
- https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.1.md#properties