¿Estás interesado en cómo construir rápidamente APIs REST sin servidor? En este artículo, voy a demostrar cómo lograr esto para NodeJS REST APIs con el Serverless Framework.
Para esta demostración, vamos a construir una simple API que gestiona los usuarios para nuestra aplicación de demostración. Para seguir adelante deberías tener los siguientes requisitos previos
- Un buen conocimiento de los fundamentos de JavaScript
- Una cuenta de AWS y credenciales de desarrollador
- Node y npm instalados localmente
Sin servidor
Creo que a estas alturas la computación sin servidor ha existido lo suficiente como para que todos seamos conscientes de que sin servidor no significa que no haya servidores.
Sólo significa que no tienes que preocuparte de gestionar y mantener tus propios servidores. Y si has tenido la mala suerte de tener que hacerlo, entonces sabes lo doloroso que puede ser a veces.
El modelo de precios también es bueno para las aplicaciones nuevas o poco utilizadas porque se cobra por el uso real en lugar de por la capacidad reservada.
Digamos que tu API atiende 1.000 solicitudes al mes. Sólo se te cobrará por el tiempo de computación que haya necesitado para ejecutar esas solicitudes.
Por otro lado, si despliegas tu aplicación en una máquina virtual EC2, se te cobrará por el mes en que esa máquina virtual funcione las 24 horas del día para dar servicio a su API.
Serverless también maneja todo el escalado de hardware automáticamente sin que tengas que intervenir en absoluto. Por lo tanto, si tu API está siendo particularmente afectada durante un período de compras navideñas o algo por el estilo, no tienes que preocuparte por escalar a más instancias de VM o actualizar el hardware. Todo eso se gestiona por ti dentro de los servicios sin servidor.
Todos los proveedores de la nube tienen alguna forma de servicio sin servidor disponible. En este artículo, vamos a utilizar Lambda de AWS para desplegar nuestra API REST sin servidor. Si quieres leer más sobre Lambda, consulta la documentación aquí.
Marco sin servidor
Puedes configurar y administrar Lambda a través de la consola de administración de AWS, pero la forma más sencilla de administrar tu código sin servidor es utilizar un marco que haga todo ese trabajo por ti.
Para este artículo, vamos a utilizar el marco sin servidor. Con este marco, puedes automatizar todos los despliegues y la gestión de los recursos de AWS que necesitas para tu código Lamda. Puedes descubrir más sobre el Serverless Framework en su sitio web.
DynamoDB
En nuestro ejemplo, utilizaremos DynamoDB para almacenar datos. DynamoDB es la versión gestionada por AWS de MongoDB, que es una base de datos sin SQL.
Básicamente, solo almacena objetos JSON como documentos, lo que la hace ideal para trabajar con aplicaciones NodeJS. También es, como he mencionado, totalmente gestionada, por lo que, al igual que Lambda, no tienes que preocuparte por el mantenimiento y el escalado. Puedes leer más sobre esto en la documentación de AWS aquí.
Ejemplo de API y DynamoDB
Ahora que hemos repasado los antecedentes de por qué elegiríamos una API sin servidor para nuestra API REST, vamos a recorrer la construcción de un ejemplo. En primer lugar:
- Tendremos que instalar la CLI de Serverless Framework.
- Ejecuta el siguiente comando en un terminal para instalarlo. Ten en cuenta que tendrás que tener NodeJS y npm instalado antes de la instalación.
$ npm install -g serverless
Con el CLI instalado ahora podemos crear un nuevo proyecto. Ejecutar el siguiente comando y que va a caminar a través de algunas indicaciones.
$ serverless
Te preguntaras si quieres crear un nuevo proyecto y la respuesta será sí. Luego te pedirá que selecciones el tipo de proyecto y nosotros seleccionaremos "AWS Node.js".
Después de hacer la selección del tipo de proyecto, nos pedirá un nombre para el proyecto y después de eso, generará el proyecto para nosotros.
Hay varias maneras de configurar las credenciales de AWS, pero por ejemplo, voy a utilizar la CLI sin servidor para proporcionar esas credenciales.
Para ello necesitarás una cuenta de AWS y un usuario IAM con un conjunto de claves de acceso para ese usuario. Utiliza el siguiente comando y sustituye las claves por las tuyas propias.
$ serverless config credentials --provider aws --key YOUR_KEY_HERE --secret YOUR_KEY_SECRET_HERE
Ten en cuenta que este comando crea un archivo con un perfil por defecto en ~/.aws/credentials, así que si necesitas actualizar tus claves más tarde o añadir un nuevo perfil, ese es el lugar para hacerlo. Sólo como referencia, el contenido de ese archivo será como el siguiente.
[default]
aws_access_key_id=YOUR_KEY_HERE
aws_secret_access_key=YOUR_SECRET_KEY_HERE
Además, ten en cuenta que si tienes varios perfiles configurados en este archivo de credenciales, puedes especificar cuál de ellos se utilizaras al realizar el despliegue mediante la CLI.
Por ejemplo, tu usarías el siguiente comando para especificar el perfil por defecto. Ten en cuenta que no tienes que especificar para obtener el perfil por defecto, pero sólo una demostración en caso de que quería usar un perfil diferente.
$ serverless deploy --aws-profile default
Dentro de nuestra carpeta de proyecto, necesitaremos crear un archivo package.json
vacío e instalar un par de dependencias.
$ npm init -y
$ npm install --save uuid
Primero, crearemos un archivo para todos nuestros manejadores de ruta. Crea una carpeta llamada routes/ y en esa carpeta crea un archivo llamado users.js. Añade el siguiente código en ese nuevo archivo.
"use strict";
const AWS = require("aws-sdk");
const uuid = require("uuid");
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.createUser = (event, context, callback) => {
const data = JSON.parse(event.body);
const params = {
TableName: process.env.DYNAMODB_TABLE,
Item: {
id: uuid.v4(),
email: data.email,
firstName: data.firstName,
lastName: data.lastName,
createdAt: new Date().getTime(),
updatedAt: new Date().getTime()
}
};
dynamoDb.put(params, error => {
if (error) {
console.error(error);
callback(null, {
statusCode: error.statusCode || 501
});
return;
}
callback(null, {
statusCode: 200,
body: JSON.stringify(params.Item)
});
});
};
module.exports.updateUser = (event, context, callback) => {
const data = JSON.parse(event.body);
const params = {
TableName: process.env.DYNAMODB_TABLE,
Key: {
id: event.pathParameters.id
},
ExpressionAttributeValues: {
":email": data.email,
":firstName": data.firstName,
":lastName": data.lastName,
":checked": data.checked,
":updatedAt": new Date().getTime()
},
UpdateExpression:
"SET email = :email, firstName = :firstName, lastName = :lastName, updatedAt = :updatedAt",
ReturnValues: "ALL_NEW"
};
dynamoDb.update(params, (error, result) => {
if (error) {
console.error(error);
callback(null, {
statusCode: error.statusCode || 501
});
return;
}
callback(null, {
statusCode: 200,
body: JSON.stringify(result.Attributes)
});
});
};
module.exports.deleteUser = (event, context, callback) => {
const params = {
TableName: process.env.DYNAMODB_TABLE,
Key: {
id: event.pathParameters.id
}
};
dynamoDb.delete(params, error => {
if (error) {
console.error(error);
callback(null, {
statusCode: error.statusCode || 501
});
return;
}
callback(null, {
statusCode: 200,
body: JSON.stringify({})
});
});
};
module.exports.getUser = (event, context, callback) => {
const params = {
TableName: process.env.DYNAMODB_TABLE,
Key: {
id: event.pathParameters.id
}
};
dynamoDb.get(params, (error, result) => {
if (error) {
console.error(error);
callback(null, {
statusCode: error.statusCode || 501
});
return;
}
callback(null, {
statusCode: 200,
body: JSON.stringify(result.Item)
});
});
};
module.exports.getUsers = (event, context, callback) => {
const params = {
TableName: process.env.DYNAMODB_TABLE
};
dynamoDb.scan(params, (error, result) => {
if (error) {
console.error(error);
callback(null, {
statusCode: error.statusCode || 501
});
return;
}
callback(null, {
statusCode: 200,
body: JSON.stringify(result.Items)
});
});
};
Este es el código de todos los gestores de rutas para la API. Tenemos un cliente de DynamoDB que se conecta a nuestra base de datos y un gestor de rutas para cada una de las operaciones CRUD.
La mayor parte de esto es bastante sencillo. Dependiendo de la operación, ejecutamos una consulta en la base de datos y enviamos la respuesta para finalizar la solicitud.
Lo siguiente que tenemos que hacer es conectar esto a los puntos finales en nuestro archivo serverless.yml
Abre ese archivo y reemplaza el contenido con lo siguiente.
service: serverless-rest-api
frameworkVersion: "2"
provider:
name: aws
runtime: nodejs12.x
environment:
DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"
functions:
createUser:
handler: routes/users.createUser
events:
- http:
path: user
method: post
cors: true
updateUser:
handler: routes/users.updateUser
events:
- http:
path: user/{id}
method: put
cors: true
deleteUser:
handler: routes/users.deleteUser
events:
- http:
path: user/{id}
method: delete
cors: true
getUser:
handler: routes/users.getUser
events:
- http:
path: user/{id}
method: get
cors: true
getUsers:
handler: routes/users.getUsers
events:
- http:
path: user
method: get
cors: true
resources:
Resources:
TodosDynamoDbTable:
Type: "AWS::DynamoDB::Table"
DeletionPolicy: Retain
Properties:
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: ${self:provider.environment.DYNAMODB_TABLE}
Este archivo es lo que Serverless utiliza para configurar los recursos en AWS cuando ejecutamos el comando serverless deploy
Puedes ver que estamos configurando dos cosas aquí. La primera es nuestra instancia de DynamoDB. Si echas un vistazo a los manejadores de ruta, también puedes ver que este nombre de tabla se expone como una variable de entorno que es consumida por nuestro cliente de DynamoDB.
También definimos una entrada para cada una de nuestras funciones Lambda. Tenemos una función para cada punto final y las asignamos todas al manejador correcto en el archivo de manejadores de ruta que acabamos de completar.
Esto completa nuestro código. Estamos listos para desplegar nuestras funciones en AWS. Ejecuta el siguiente comando para iniciar el despliegue.
$ serverless deploy
Esto tomará un poco para configurar todo y luego imprimirá las ubicaciones de los puntos finales de la API en la terminal una vez que haya terminado.
Ahora que tenemos los endpoints podemos probarlos todos en un cliente HTTP. Vea algunos ejemplos a continuación.
Cuando hayas terminado de jugar con la API recuerda ejecutar el siguiente comando para desmontar todo de nuestra aplicación de ejemplo en AWS.
$ serverless remove
La computación sin servidor es genial para pequeñas aplicaciones o nuevos prototipos para ayudar a moverse muy rápidamente. Y espero que esta demostración de cómo hacerlo con el Serverless Framework pueda facilitarte aún más las cosas a los desarrolladores. ¡Gracias por leer!