Crud Dynamodb usando funciones lambdas y apigateway
Tags: crud, apigateway, dynamo, sam
Published: 2022-12-04 18:55:51

**Amazon Web Services**

La plataforma en la nube Amazon Web Services (AWS) ofrece múltiples servicios integrales para que los desarrolladores de software puedan implementar sus soluciones de manera rápida y relativamente fácil.

Hoy en día es indispensable crear servicios web para entregar y capturar información para que las aplicaciones puedan conectarse entre ellas, los servicios rest son de los más usados actualmente.

## Ejercicio práctico

Para este ejemplo se crea una api rest que realice operaciones CRUD (Crear, Leer, Actualizar y Eliminar) de productos dentro de una base de datos NoSQL proporcionada por AWS, se implementa funciones lambdas para realizar el código y el servicio de Apigateway para consumir desde un cliente.

**Requisitos previos:**

- Cuenta activa de AWS
- Tener configurada las credenciales AWS (AWS CLI y SAM CLI) en el ordenador.
- Conocimientos esenciales de serverless.
- Conocimientos esenciales de Cloudformation.
- Tener instalado nodejs (para este ejercicio se usa v16)

**Tecnologías a usar en AWS**

- AWS CLI y SAM CLI
- Cloudformation
- Lambdas (Nodejs)
- Dynamodb
- Apigateway

A continuación se detalla brevemente y se pone en contexto de las herramientas de AWS a usar.

**Serverless**

Serverless quiere decir sin servidor, permite al desarrollador crear y ejecutar aplicaciones con rapidez y menor costo, no se preocupa por gestionar infraestructura de servidores donde se aloja el código, simplemente se enfoca en codear. Puedes revisar más del tema en este artículo: [Serverless.](https://dev.to/matiasfha/que-es-serverless-4ck9)

**AWS CLI y SAM CLI**

AWS CLI es una herramienta de línea de comandos de AWS, se usa para administrar sus servicios en AWS. Se debe configurar las credenciales con aws configure, ver el siguiente enlace:[AWS CLI.](https://docs.aws.amazon.com/es_es/cli/latest/userguide/cli-configure-quickstart.html)

SAM

Significa modelo de aplicación sin servidor de AWS, es un framework que se utiliza para crear aplicaciones sin servidor en AWS. También tiene una interfaz de línea de comandos para poder desplegar servicios, revisar el siguiente link para su instalación dependiendo del sistema operativo: [SAM CLI.](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html)

**Cloudformation**

AWS CloudFormation es un servicio que ofrece a desarrolladores una manera sencilla de crear una colección de recursos de AWS y organizarlos como si fuese archivos de código.

**Lambdas**

AWS Lambda permite correr código sin administrar servidores, el desarrollador solo escribe trozos de código (funciones) y se las sube a estos servicios, se puede usar con varios lenguajes entre ellos: python, javascript y java. Para más información revisar: [AWS Lambda.](https://docs.aws.amazon.com/lambda/index.html)

**Dynamodb**

Es un servicio de base de datos NoSQL de AWS, permite almacenar datos en documentos y valores clave.

**Api Gateway**

Amazon Api Gateway es un servicio de AWS para administrar y proteger servicios de API REST, HTTP y WebSockets. Permite conectarse a otros servicios y consumirlas mediante una url.
Para más información revisar el siguiente enlace: [AWS Api Gateway.](https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html)

**Infraestructura a crear**

Se indica un pequeño diagrama de como va a funcionar la aplicación CRUD a crear:

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

## Inicio de proyecto

Dentro de un directorio en la consola colocar el comando sam init y completar las instrucciones de la siguiente manera:

![Console 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/63yt34eyobyz8ajafzlf.png)

![Console 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p5lbhgr4qlu76g9esalc.png)

Se debe generar un proyecto en la siguiente estructura:

![Estructura de proyecto](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2i4ls3o536qhqar5q4bz.png)

Esta estructura solo nos sirve como ejemplo de partida, para un mejor control del proyecto se configura de la siguiente manera, quitar la carpeta de tests y el archivo principal app.ts, la carpeta hello-world se cambia por app:

![New infra](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2ex2ya2aq2s986y7zi1g.png)

**Instalar dependencias**

Dentro de la carpeta app en donde se encuentra el package.json ejecutar lo siguiente:

``` sh
npm install
npm install aws-sdk
npm install short-uuid
```

**DynamoDB, servicios y modelo**

En las siguientes carpteas crear los archivos:

![Services](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ngo0p4zg56c4276ci3mu.png)

**DynamoClient.ts**

Nos genera una instancia cliente para poder realizar operaciones en una tabla de Dynamodb.

``` js
import DynamoDB from 'aws-sdk/clients/dynamodb';
export const dynamoClient = new DynamoDB.DocumentClient();
```
**Product.ts**

Es un molde de los campos que va a contener el producto.

``` ts
export interface Product{
    id: string;
    name: string;
    price: number;
    description: string;
}
```

**ProductService.ts**

Es una clase que nos va a permitir realizar la lógica de cada una de las funciones que estamos usando, en este caso operaciones CRUD:

``` js
import { dynamoClient } from "../dynamodb/DynamoClient";
import { Product } from '../models/Product';
import { generate } from 'short-uuid';


export class ProductService {

    static async getProducts(): Promise<Product[]> {
        try {
            const tableName = process.env.TABLE_NAME ?? ''
            const response = await dynamoClient.scan({
                TableName: tableName
            }).promise();

            return response.Items as Product[];
        } catch (error) {
            throw new Error("Error getting products: " + error);
        }
    }

    static async getProductById(id: string): Promise<Product> {
        try {
            const tableName = process.env.TABLE_NAME ?? ''
            const response = await dynamoClient.get({
                TableName: tableName,
                Key: {
                    id
                }
            }).promise();
            return response.Item as Product;
        } catch (error) {
            throw new Error("Error getting product by id: " + error);
        }
    }

    static async createProduct(product: Product): Promise<Product> {
        try {
            const tableName = process.env.TABLE_NAME ?? ''
            product.id = generate();
            await dynamoClient.put({
                TableName: tableName,
                Item: product
            }).promise();
            return product;
        } catch (error) {
            throw new Error("Error creating product: " + error);
        }
    }

    static async updateProductById(product: Product): Promise<Product> {
        try {
            const tableName = process.env.TABLE_NAME ?? ''
            await dynamoClient.update({
                TableName: tableName,
                Key: {
                    id: product.id
                },
                UpdateExpression: "set #name = :name, #price = :price, #description = :description",
                ExpressionAttributeNames: {
                    "#name": "name",
                    "#price": "price",
                    "#description": "description"
                },
                ExpressionAttributeValues: {
                    ":name": product.name,
                    ":price": product.price,
                    ":description": product.description
                }
            }).promise();
            return product;
        } catch (error) {
            throw new Error("Error updating product: " + error);
        }
    }

    static async deleteProductById(id: string): Promise<void> {
        try {
            const tableName = process.env.TABLE_NAME ?? ''
            await dynamoClient.delete({
                TableName: tableName,
                Key: {
                    id
                }
            }).promise();
        } catch (error) {
            throw new Error("Error deleting product: " + error);
        }
    }

}
```

**Funciones**

Dentro de la carpeta funciones se crea un archivo por cada función.

![Funciones.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4qa2wqt2s2gh018r0j63.png)


En las funciones solo hace falta llamar al servicio creado anteriormente y retornar datos:

**create.ts**

``` js
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { ProductService } from '../services/ProductService';
import { Product } from '../models/Product';

/**
 * Function to create a new product
 * @param event 
 * @returns 
 */
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const newProduct: Product = JSON.parse(event.body as string);
    const product = await ProductService.createProduct(newProduct);
    return {
        statusCode: 201,
        body: JSON.stringify({
            item: product
        })
    }
}
```
**deleteById.ts**

``` ts
import { ProductService } from '../services/ProductService';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

/**
 * Function to delete a product by id
 * @param event 
 * @returns 
 */
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const id = event.pathParameters?.id;
    if (!id) {
        return {
            statusCode: 400,
            body: JSON.stringify({
                message: "id is required"
            })
        }
    }
    await ProductService.deleteProductById(id);
    return {
        statusCode: 204,
        body: 'Product deleted'
    }
}
```
**getAll.ts**

``` js
import { ProductService } from '../services/ProductService';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

/**
 * Function to get all products
 * @param event 
 * @returns 
 */
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const products = await ProductService.getProducts();
    return {
        statusCode: 200,
        body: JSON.stringify({
            items: products
        })
    }
}
```
**getById.ts**

``` js
import { ProductService } from '../services/ProductService';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

/**
 * 
 * @param event Function to get product by id
 * @returns 
 */
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const id = event.pathParameters?.id;
    if (!id) {
        return {
            statusCode: 400,
            body: JSON.stringify({
                message: "id is required"
            })
        }
    }
    const product = await ProductService.getProductById(id);
    return {
        statusCode: 200,
        body: JSON.stringify({
            item: product
        })
    }
}
```

**updateById.ts**

``` js
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { ProductService } from '../services/ProductService';
import { Product } from '../models/Product';

/**
 * Function to update a product by id
 * @param event 
 * @returns 
 */
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
    const updatedProduct: Product = JSON.parse(event.body as string);
    const product = await ProductService.updateProductById(updatedProduct);
    return {
        statusCode: 200,
        body: JSON.stringify({
            item: product
        })
    }
}
```

**Template.yaml**

En este archivo se configura toda la infraestructura que se va a desplegar en AWS: lambdas, apigateway, roles, tablas, etc. La configuración es cloudformation con características avanzadas proporcionadas por SAM para desplegar aplicaciones serverless.

Se configura las propiedades de algunos recursos de manera global para evitar repetir cosas:

![Globals](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r0miqjefh0res4dlcqqh.png)

En los recursos para cada una de las funciones debe quedar de la siguiente manera:

![Function resource](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2nlkuh9cjtvh4dh6c96d.png)

La parte de Policies es muy importante, se le da permisos a la función lambda para ejecutar operaciones en la tabla de dynamodb que estamos creando.

A continuación se muestra el template completo:

``` yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  crud-products-serverless

  Sample SAM Template for crud-products-serverless
  
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    CodeUri: app
    Timeout: 10
    Tracing: Active
    Runtime: nodejs16.x
    Architectures:
      - x86_64
    Environment:
      Variables:
        TABLE_NAME: !Ref ProductTable
    
  Api:
    TracingEnabled: True

Resources:
  CreateProductFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/functions/create.handler
      Events:
        CreateProduct:
          Type: Api
          Properties:
            Path: /products
            Method: post
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref ProductTable
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: "es2020"
        EntryPoints: 
        - src/functions/create.ts

  UpdateProductFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/functions/updateById.handler
      Events:
        CreateProduct:
          Type: Api
          Properties:
            Path: /products
            Method: put
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref ProductTable
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: "es2020"
        EntryPoints: 
        - src/functions/updateById.ts

  DeleteByIdProductFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/functions/deleteById.handler
      Events:
        CreateProduct:
          Type: Api
          Properties:
            Path: /products/{id}
            Method: delete
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref ProductTable
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: "es2020"
        EntryPoints: 
        - src/functions/deleteById.ts

  GetAllProductsFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/functions/getAll.handler
      Events:
        CreateProduct:
          Type: Api
          Properties:
            Path: /products
            Method: get
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref ProductTable
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: "es2020"
        EntryPoints: 
        - src/functions/getAll.ts

  GetProductByIdFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/functions/getById.handler
      Events:
        CreateProduct:
          Type: Api
          Properties:
            Path: /products/{id}
            Method: get
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref ProductTable
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: "es2020"
        EntryPoints: 
        - src/functions/getById.ts

  ProductTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub ${AWS::StackName}-products
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      BillingMode: PAY_PER_REQUEST

```

**Despliegue**

Eso sería todo en cuanto a las plantillas y el código para realizar operaciones CRUD. Dentro de la carpeta donde está la plantilla template.yaml ejecutar:

``` sh
sam build
```

Seguidamente ejecutar:

``` sh
sam deploy --guided
```
A continuación realiza algunas preguntas, se recomienda configurar de la siguiente manera:

![sam deploy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/358shrnwaxbwhovlfqvv.png)

Luego confirmar el despliegue y esperar:

![Despliegue](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e8qsu9uzkuvmp8jb5dgv.png)

Es probable que en windows la consola se queda congelada, revisar en la consola aws directamente.

Se revisa en la consola de cloudformation de AWS:

![Stack desplegado](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sw35z2uur5zfhtdc48d0.png)

Los recursos han sido creados:

![Recursos creados](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gyf1xokcqt0feafabz1i.png)

En el recurso de apigateway se puede ver la implementación para poder consumirlas.

![Api gateway](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a1ki6qvitymf7wa48huv.png)

![Apigateway](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ldb2sc10axw54pdaqs3.png)

**Probando apis**

Consultando productos (se creo uno previamente):

![Prueba 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hsmianp3h043wi7wwaxn.png)

Se crea un producto:

![Prueba 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3j5zcbykl3kkwwn3llna.png)

Producto por ID.:

![Prueba 3](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xc0gdrm80imw94kaa0jq.png)

**Despedida**

Eso sería todo, puedes ir probando todas las demás funciones, de esta manera es sencillo desplegar apis con AWS. Si alguna duda no dudes en comentarlo. 

## Referencias

- https://aws.amazon.com/cli/
- https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html
- https://aws.amazon.com/es/cloudformation/faqs/#:~:text=AWS%20CloudFormation%20es%20un%20servicio,de%20forma%20ordenada%20y%20predecible.