La guía completa para crear una API con TypeScript y AWS

En este artículo veremos cómo podemos construir rápida y fácilmente una API con TypeScript y Serverless.

Luego, aprenderemos a usar aws-sdk para acceder a otros servicios de AWS y crear una API de traducción automática.

Si prefiere ver y aprender, puede ver el video a continuación:

Empezando

Para comenzar todo este proceso, debemos asegurarnos de tener instalado Serverless Framework y tener un perfil de AWS configurado en nuestra computadora. Si no lo ha hecho, puede ver este video sobre cómo configurarlo todo.

Si desea seguir este tutorial, puede seguir todos los pasos o descargar el código aquí y seguir con el código completo.

Ahora estamos en la creación de nuestro proyecto sin servidor y API. Necesitamos comenzar en una terminal y ejecutar el comando para crear nuestro nuevo repositorio. Todo lo que necesita hacer es cambiar el {YOUR FOLDER NAME}por el nombre de su carpeta.

serverless create --template aws-nodejs-typescript --path {YOUR FOLDER NAME}

Esto creará un proyecto sin servidor muy básico con TypeScript. Si abrimos esta nueva carpeta con VS Code entonces podemos ver lo que nos ha dado la plantilla.

Los archivos principales que queremos ver son el serverless.tsarchivo y el handler.tsarchivo.

El serverless.tsarchivo es donde se guarda la configuración para la implementación. Este archivo le dice al framework sin servidor el nombre del proyecto, el lenguaje de ejecución del código, la lista de funciones y algunas otras opciones de configuración.

Siempre que queramos cambiar la arquitectura de nuestro proyecto, este es el archivo en el que trabajaremos.

El siguiente archivo es el handler.tsarchivo. Aquí tenemos el código de ejemplo para una lambda que nos proporciona la plantilla. Es muy básico y solo devuelve una respuesta de API Gateway con un mensaje y el evento de entrada. Usaremos esto más adelante como un bloque de inicio para nuestra propia API.

Crea tu propia Lambda

Ahora que hemos visto lo que obtenemos con la plantilla, es hora de agregar nuestro propio punto final de Lambda y API.

Para empezar, vamos a crear una nueva carpeta para contener todo nuestro código lambda y llamarlo lambdas. Esto ayuda a organizarlo, especialmente cuando comienza a obtener algunas lambdas diferentes en un proyecto.

En esa nueva carpeta vamos a crear nuestra nueva lambda llamándola getCityInfo.ts. Si abrimos este archivo podemos empezar a crear nuestro código. Podemos empezar copiando todo el handler.tscódigo como punto de partida.

Lo primero que vamos a hacer es cambiar el nombre de la función a handler. Esta es una preferencia personal, pero me gusta nombrar la función que maneja el controlador de eventos.

En la primera línea de esta función necesitamos agregar algún código para obtener la ciudad que el usuario solicita. Podemos obtener esto de la ruta URL usando pathParameters.

const city = event.pathparameter?.city;

Una cosa que puede notar es el uso de ?.en esa declaración. Eso es el encadenamiento opcional y es una característica realmente interesante. yo

t significa que si el parámetro de ruta es verdadero, obtenga el parámetro de ciudad, de lo contrario, devuelva indefinido. Esto significa que si pathParameterno fuera un objeto , no obtendría el error que causa el error en el tiempo de ejecución del nodo.cannot read property city of undefined

Ahora que tenemos la ciudad, debemos verificar que la ciudad sea válida y que tengamos datos para esa ciudad. Para ello necesitamos algunos datos. Podemos usar el siguiente código y pegarlo en la parte inferior del archivo.

interface CityData { name: string; state: string; description: string; mayor: string; population: number; zipCodes?: string; } const cityData: { [key: string]: CityData } = { newyork: { name: 'New York', state: 'New York', description: 'New York City comprises 5 boroughs sitting where the Hudson River meets the Atlantic Ocean. At its core is Manhattan, a densely populated borough that’s among the world’s major commercial, financial and cultural centers. Its iconic sites include skyscrapers such as the Empire State Building and sprawling Central Park. Broadway theater is staged in neon-lit Times Square.', mayor: 'Bill de Blasio', population: 8399000, zipCodes: '100xx–104xx, 11004–05, 111xx–114xx, 116xx', }, washington: { name: 'Washington', state: 'District of Columbia', description: `DescriptionWashington, DC, the U.S. capital, is a compact city on the Potomac River, bordering the states of Maryland and Virginia. It’s defined by imposing neoclassical monuments and buildings – including the iconic ones that house the federal government’s 3 branches: the Capitol, White House and Supreme Court. It's also home to iconic museums and performing-arts venues such as the Kennedy Center.`, mayor: 'Muriel Bowser', population: 705549, }, seattle: { name: 'Seattle', state: 'Washington', description: `DescriptionSeattle, a city on Puget Sound in the Pacific Northwest, is surrounded by water, mountains and evergreen forests, and contains thousands of acres of parkland. Washington State’s largest city, it’s home to a large tech industry, with Microsoft and Amazon headquartered in its metropolitan area. The futuristic Space Needle, a 1962 World’s Fair legacy, is its most iconic landmark.`, mayor: 'Jenny Durkan', population: 744955, }, };

La diferencia entre esto y JavaScript es que podemos crear una interfaz para decirle al sistema cuál debe ser la estructura de los datos. Esto se siente como un trabajo extra al principio, pero ayudará a que todo sea más fácil más adelante.

Dentro de nuestra interfaz definimos las claves del objeto ciudad; algunos que son cadenas, un número y luego zipCodeses una propiedad opcional. Esto significa que podría estar ahí, pero no tiene por qué estarlo.

Si queremos probar nuestra interfaz, podemos intentar agregar una nueva propiedad a cualquiera de los datos de nuestras ciudades.

TypeScript debería decirle instantáneamente que su nueva propiedad no existe en la interfaz. Si elimina una de las propiedades requeridas, TypeScript también se quejará. Esto asegura que siempre tenga los datos correctos y los objetos siempre se vean exactamente como se espera.

Ahora que tenemos los datos, podemos verificar si el usuario envió la solicitud de ciudad correcta.

if (!city || !cityData[city]) { }

Si esta afirmación es verdadera, entonces el usuario ha hecho algo mal, por lo tanto, debemos devolver una respuesta de 400.

Podríamos simplemente escribir manualmente el código aquí, pero vamos a crear un nuevo apiResponsesobjeto con métodos para algunos de los posibles códigos de respuesta de la API.

const apiResponses = { _200: (body: { [key: string]: any }) => { return { statusCode: 200, body: JSON.stringify(body, null, 2), }; }, _400: (body: { [key: string]: any }) => { return { statusCode: 400, body: JSON.stringify(body, null, 2), }; }, };

Esto solo hace que sea mucho más fácil de reutilizar más adelante en el archivo. También debería ver que tenemos una propiedad de body: { [key: string]: any }. Esto indica que esta función tiene una propiedad de cuerpo que debe ser un objeto. Ese objeto puede tener claves que tengan un valor de cualquier tipo.

Porque sabemos que bodysiempre será una cadena que podemos usar JSON.stringifypara asegurarnos de devolver un cuerpo de cadena.

Si agregamos esta función a nuestro controlador, obtenemos esto:

export const handler: APIGatewayProxyHandler = async (event, _context) => { const city = event.pathParameters?.city; if (!city || !cityData[city]) { return apiResponses._400({ message: 'missing city or no data for that city' }); } return apiResponses._200(cityData[city]); };

Si el usuario no rechazó una ciudad o una de la que no tenemos datos, devolvemos un 400 con un mensaje de error. Si los datos existen, devolvemos un 200 con un cuerpo de datos.

Agregar una nueva API de traducción

En la sección anterior, configuramos nuestro repositorio de API de TypeScript y creamos una lambda que solo usaba datos codificados.

This part is going to teach you how to use the aws-sdk to interact directly with other AWS services to create a really powerful API.

To start, we need to add a new file for our translation API. Create a new file under the lambdas folder called translate.ts. We can start this file out with some basic boilerplate code. This is the starting code for a TypeScript API Lambda.

import { APIGatewayProxyHandler } from 'aws-lambda'; import 'source-map-support/register'; export const handler: APIGatewayProxyHandler = async (event) => { };

Now we need to get the text that the user wants translated and the language that they want to translate to. We can get these from the body of the request.

One extra thing we have to do here is to parse the body. By default, API Gateway stringifies any JSON passed in the body. We can then destructure the text and language from the body.

const body = JSON.parse(event.body); const { text, language } = body;

We now need to check that the user has passed up text and language.

if (!text) { // retrun 400 } if (!language) { // return 400 }

In the last part we created the 400 response as a function in the file. As we're going to be using these API responses across multiple files, it is a good idea to pull them out to their own common file.

Create a new folder under lambdas called common. This is where we are going to store all common functions.

In that folder create a new file called apiResponses.ts. This file is going to export the apiResponses object with the _200 and _400 methods on it. If you have to return other response codes then you can add them to this object.

const apiResponses = { _200: (body: { [key: string]: any }) => { return { statusCode: 200, body: JSON.stringify(body, null, 2), }; }, _400: (body: { [key: string]: any }) => { return { statusCode: 400, body: JSON.stringify(body, null, 2), }; }, }; export default apiResponses;

We can now import that object into our code and use these common methods in our code. At the top of our translate.ts file we can now add this line:

import apiResponses from './common/apiResponses';

and update our text and language checks to call the _400 method on that object:

if (!text) { return apiResponses._400({ message: 'missing text fom the body' }); } if (!language) { return apiResponses._400({ message: 'missing language from the body' }); }

With that completed we know that we have the text to translate and a language to translate into, so we can start the translation process.

Using the aws-sdk is almost always an async task so we're going to wrap it in a try/catch so that our error handling is easier.

try { } catch (error) { }

The first thing we need to do is to import the aws-sdk and create a new instance of the translate service.

To do that we need to install the aws-sdk and then import it. First run npm install --save aws-sdk and then add this code to the top of your translate file:

import * as AWS from 'aws-sdk'; const translate = new AWS.Translate();

With this we can start to write our translation code. We're going to start with the line that does the translation first. Add this in the try section.

const translatedMessage = await translate.translateText(translateParams).promise();

One thing that some of you may have noticed is that we're passing in translateParams without having defined it yet. That is because we're not sure what type it is yet.

To find this out we can use a tool in VS Code called go to definition. This allows us to jump to where the function if defined so we can find out what the type of the parameters is. You can either right click and select go to definition or hold Ctrl and click on the function.

As you can see the translateText function takes a param of Translate.Types.TranslateTextRequest.

Another way to find this out is to use intelisense by mousing over the translateText function. You should see this, where you can see that params: AWS.Translate.TranslateTextRequest:

With this we can create our translate params above the translate request we made earlier. We can then populate it based on the type we are setting it as. This makes sure we're passing up the correct fields.

const translateParams: AWS.Translate.Types.TranslateTextRequest = { Text: text, SourceLanguageCode: 'en', TargetLanguageCode: language, };

Now that we have the parameters and are passing them into the translate.translateText function, we can start creating our response. This is just going to be a 200 response with the translated message.

return apiResponses._200({ translatedMessage });

With that all done we can move onto the catch section. In here we just want to log out the error and then return a 400 response from the common file.

console.log('error in the translation', error); return apiResponses._400({ message: 'unable to translate the message' });

With that completed we're done with our lambda code, so need to move into our severless.ts file to add this new API endpoint and give it the permissions it needs.

In the serverless.ts file we can scroll down to the functions section. In here we need to add a new function to the object.

translate: { handler: 'lambdas/translate.handler', events: [ { http: { path: 'translate', method: 'POST', cors: true, }, }, ], },

The main difference between this and the previous endpoint is that the endpoint is now a POST method. This means if you try and do a GET request to this URL path, you'll get an error response.

The last thing to do is to give the lambdas permission to use the Translate service. With almost all of the AWS Services, you'll need to add extra permissions to be able to use the from within a lambda.

To do this we add a new field onto the provider section called iamRoleStatements. This is an array of allow or deny statements for different services and resources.

iamRoleStatements: [ { Effect: 'Allow', Action: ['translate:*'], Resource: '*', }, ],

With this added in we have everything we need set up so we can run sls deploy to deploy our new API.

Once this has deployed, we can get the API URL and use a tool like postman or postwoman.io to make a request to that URL. We just need to pass up a body of:

{ "text": "This is a test message for translation", "language": "fr" }

and then we should get a 200 response of:

{ "translatedMessage": { "TranslatedText": "Ceci est un message de test pour la traduction", "SourceLanguageCode": "en", "TargetLanguageCode": "fr" } }

Summary

In this article we've learnt how to:

  • Set up a new TypeScript repo with severless create --template aws-nodejs-typescript
  • Add our own Lambda that returns a selection of hardcoded data
  • Added that Lambda as an API endpoint
  • Added another Lambda which will automatically translate any text passed to it
  • Added an API endpoint and gave the Lambda the permissions it needed to work

If you enjoyed this article and want to learn more about Serverless and AWS, then I have a Youtube Channel with over 50 videos on all of this. I'd recommend watching the videos you find most interesting in my Serverless and AWS playlist.