Trabajar con compatibilidad con TypeScript y Jest: un tutorial de AWS SAM

Publicado: 2022-03-11

Una poderosa herramienta para crear aplicaciones sin servidor, el Modelo de aplicación sin servidor (SAM) de AWS se empareja con frecuencia con JavaScript: el 62 % de los desarrolladores de medianas y grandes empresas eligen JavaScript para su código sin servidor. Sin embargo, TypeScript está ganando popularidad y supera con creces a JavaScript como el tercer lenguaje más querido por los desarrolladores.

Si bien el código estándar de JavaScript no es difícil de encontrar, iniciar proyectos de AWS SAM con TypeScript es más complejo. El siguiente tutorial muestra cómo crear un proyecto TypeScript de AWS SAM desde cero y cómo las diferentes partes funcionan juntas. Los lectores solo necesitan estar algo familiarizados con las funciones de AWS Lambda para seguir.

Inicio de nuestro proyecto AWS SAM TypeScript

La base de nuestra aplicación sin servidor incluye varios componentes. Primero configuraremos el entorno de AWS, nuestro paquete npm y la funcionalidad de Webpack; luego, podremos crear, invocar y probar nuestra función Lambda para ver nuestra aplicación en acción.

Preparar el Ambiente

Para configurar el entorno de AWS, necesitamos instalar lo siguiente:

  1. CLI de AWS
  2. CLI de SAM de AWS
  3. Node.js y npm

Tenga en cuenta que este tutorial requiere instalar Docker durante el paso 2 anterior para probar nuestra aplicación localmente.

Inicializar un proyecto vacío

Vamos a crear el directorio del proyecto, aws-sam-typescript-boilerplate y una subcarpeta src para guardar el código. Desde el directorio del proyecto, configuraremos un nuevo paquete npm:

 npm init -y # -y option skips over project questionnaire

Este comando creará un archivo package.json dentro de nuestro proyecto.

Agregar la configuración del paquete web

Webpack es un paquete de módulos que se utiliza principalmente para aplicaciones de JavaScript. Dado que TypeScript se compila en JavaScript simple, Webpack preparará efectivamente nuestro código para el navegador web. Instalaremos dos bibliotecas y un cargador personalizado:

  • paquete web: biblioteca central
  • webpack-cli: utilidades de línea de comandos para Webpack
  • ts-loader: cargador TypeScript para Webpack
 npm i --save-dev webpack webpack-cli ts-loader

El comando de compilación de la CLI de AWS SAM, sam build , ralentiza el proceso de desarrollo porque intenta ejecutar npm install para cada función, lo que provoca la duplicación. Usaremos un comando de compilación alternativo de la biblioteca aws-sam-webpack-plugin para acelerar nuestro entorno.

 npm i --save-dev aws-sam-webpack-plugin

De forma predeterminada, Webpack no proporciona un archivo de configuración. Hagamos un archivo de configuración personalizado llamado webpack.config.js en la carpeta raíz:

 /* eslint-disable @typescript-eslint/no-var-requires */ const path = require('path'); const AwsSamPlugin = require('aws-sam-webpack-plugin'); const awsSamPlugin = new AwsSamPlugin(); module.exports = { entry: () => awsSamPlugin.entry(), output: { filename: (chunkData) => awsSamPlugin.filename(chunkData), libraryTarget: 'commonjs2', path: path.resolve('.') }, devtool: 'source-map', resolve: { extensions: ['.ts', '.js'] }, target: 'node', mode: process.env.NODE_ENV || 'development', module: { rules: [{ test: /\.tsx?$/, loader: 'ts-loader' }] }, plugins: [awsSamPlugin] };

Ahora examinemos las distintas partes:

  • entry : esto carga el objeto de entrada (donde Webpack comienza a crear el paquete) desde el recurso AWS::Serverless::Function .
  • output : Esto apunta al destino de la salida de compilación (en este caso, .aws-sam/build ). Aquí también especificamos la biblioteca de destino como commonjs2 , que asigna el valor de retorno del punto de entrada a module.exports . Este punto de entrada es el predeterminado para los entornos de Node.js.
  • devtool : esto crea un mapa de origen, app.js.map , en nuestro destino de salida de compilación. Asigna nuestro código original al código que se ejecuta en el navegador web y ayudará con la depuración si establecemos la variable de entorno NODE_OPTIONS en --enable-source-maps para nuestro Lambda.
  • resolve : Esto le dice a Webpack que procese los archivos TypeScript antes que los archivos JavaScript.
  • target : Esto le dice a Webpack que apunte a Node.js como nuestro entorno. Esto significa que Webpack utilizará la función require de Node.js para cargar fragmentos cuando se compile.
  • module : esto aplica el cargador TypeScript a todos los archivos que cumplen con la condición de test . En otras palabras, garantiza que el cargador manejará todos los archivos con una extensión .ts o .tsx .
  • plugins : esto ayuda a Webpack a identificar y usar nuestro aws-sam-webpack-plugin .

En la primera línea, hemos deshabilitado una regla ESLint particular para este archivo. Las reglas estándar de ESLint que configuraremos más adelante desaconsejan el uso de la instrucción require . Preferimos require import en Webpack, por lo que haremos una excepción.

Configurar la compatibilidad con TypeScript

Agregar compatibilidad con TypeScript mejorará la experiencia del desarrollador al:

  • Prevención de mensajes de advertencia sobre declaraciones de tipos faltantes.
  • Proporcionar validación de tipo.
  • Ofreciendo autocompletado dentro del IDE.

Primero, instalaremos TypeScript para nuestro proyecto localmente (omita este paso si tiene TypeScript instalado globalmente):

 npm i --save-dev typescript

Incluiremos los tipos para las bibliotecas que estamos usando:

 npm i --save-dev @types/node @types/webpack @types/aws-lambda

Ahora, crearemos el archivo de configuración de TypeScript, tsconfig.json , en la raíz del proyecto:

 { "compilerOptions": { "target": "ES2015", "module": "commonjs", "sourceMap": true, "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, }, "include": ["src/**/*.ts", "src/**/*.js"], "exclude": ["node_modules"] }

Aquí estamos siguiendo la configuración predeterminada recomendada por la comunidad de TypeScript. Hemos agregado include para agregar los archivos en la carpeta src al programa y exclude para evitar la compilación de TypeScript para la carpeta node_modules ; no tocaremos este código directamente.

Crear una función Lambda

No hemos escrito ningún código Lambda para nuestra aplicación sin servidor hasta ahora, así que comencemos. En la carpeta src que creamos anteriormente, crearemos una subcarpeta test-lambda que contiene un archivo app.ts con esta función Lambda:

 import { APIGatewayEvent } from 'aws-lambda'; export const handler = async (event: APIGatewayEvent) => { console.log('incoming event is', JSON.stringify(event)); const response = { statusCode: 200, body: JSON.stringify({ message: 'Request was successful.' }) }; return response; };

Esta función de marcador de posición simple devuelve una respuesta 200 con un cuerpo. Podremos ejecutar el código después de un paso más.

Incluir el archivo de plantilla de AWS

AWS SAM requiere un archivo de plantilla para transpilar nuestro código e implementarlo en la nube. Cree el archivo template.yaml en la carpeta raíz:

 AWSTemplateFormatVersion: "2010-09-09" Transform: AWS::Serverless-2016-10-31 Description: AWS SAM Boilerplate Using TypeScript Globals: Function: Runtime: nodejs14.x # modify the version according to your need Timeout: 30 Resources: TestLambda: Type: AWS::Serverless::Function Properties: Handler: app.handler FunctionName: "Test-Lambda" CodeUri: src/test-lambda/ Events: ApiEvent: Type: Api Properties: Path: /test Method: get

Este archivo de plantilla genera una función Lambda a la que se puede acceder desde una API HTTP GET. Tenga en cuenta que la versión a la que se hace referencia en Runtime: puede necesitar personalización.

Ejecute la aplicación

Para ejecutar la aplicación, debemos agregar un nuevo script en el archivo package.json para construir el proyecto con Webpack. El archivo puede tener scripts existentes, como un script de prueba vacío. Podemos agregar el script de compilación de esta manera:

 "scripts": { "build": "webpack-cli" }

Si ejecuta npm run build desde la raíz del proyecto, debería ver la carpeta de compilación, .aws-sam , creada. Aquellos de nosotros en un entorno Mac puede que necesitemos hacer visibles los archivos ocultos presionando Comando + Shift + . para ver la carpeta.

Ahora iniciaremos un servidor HTTP local para probar nuestra función:

 sam local start-api

Cuando visitamos el punto final de prueba en un navegador web, deberíamos ver un mensaje de éxito.

El navegador web muestra el enlace "127.0.0.1:3000/test" en la barra de direcciones. Debajo de la barra de direcciones, la página web está en blanco excepto por un mensaje que dice '{"mensaje": "La solicitud fue exitosa".}.

La consola debería mostrar que la función se monta en un contenedor Docker antes de ejecutarse, razón por la cual instalamos Docker anteriormente:

 Invoking app.handler (nodejs14.x) Skip pulling image and use local one: public.ecr.aws/sam/emulation-nodejs14.x:rapid-1.37.0-x86_64. Mounting /Users/mohammadfaisal/Documents/learning/aws-sam-typescript-boilerplate/.aws-sam/build/TestLambda as /var/task:ro, delegated inside runtime container

Mejorando nuestro flujo de trabajo de desarrollo para un entorno profesional

Nuestro proyecto está en funcionamiento, agregar algunos toques finales garantizará una experiencia de desarrollador excepcional que impulsará la productividad y la colaboración.

Optimice la compilación con recarga en caliente

Es tedioso ejecutar el comando de compilación después de cada cambio de código. La recarga en caliente solucionará este problema. Podemos agregar otra secuencia de comandos en nuestro package.json para observar los cambios en los archivos:

 "watch": "webpack-cli -w"

Abra una terminal separada y ejecute npm run watch . Ahora, su proyecto se compilará automáticamente cuando cambie cualquier código. Modifique el mensaje del código, actualice su página web y vea el resultado actualizado.

Mejore la calidad del código con ESLint y Prettier

Ningún proyecto de TypeScript o JavaScript está completo sin ESLint y Prettier. Estas herramientas mantendrán la calidad y consistencia del código de su proyecto.

Instalemos primero las dependencias principales:

 npm i --save-dev eslint prettier

Agregaremos algunas dependencias auxiliares para que ESLint y Prettier puedan trabajar juntos en nuestro proyecto TypeScript:

 npm i --save-dev \ eslint-config-prettier \ eslint-plugin-prettier \ @typescript-eslint/parser \ @typescript-eslint/eslint-plugin

A continuación, agregaremos nuestro linter creando un archivo de configuración de ESLint, .eslintrc , dentro de la raíz del proyecto:

 { "root": true, "env": { "es2020": true, "node": true, "jest": true }, "parser": "@typescript-eslint/parser", "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended" ], "ignorePatterns": ["src/**/*.test.ts", "dist/", "coverage/", "test/"], "parserOptions": { "ecmaVersion": 2018, "sourceType": "module", "ecmaFeatures": { "impliedStrict": true } }, "rules": { "quotes": ["error", "single", { "allowTemplateLiterals": true }], "default-case": "warn", "no-param-reassign": "warn", "no-await-in-loop": "warn", "@typescript-eslint/no-unused-vars": [ "error", { "vars": "all", "args": "none" } ] }, "settings": { "import/resolver": { "node": { "extensions": [".js", ".jsx", ".ts", ".tsx"] } } } }

Tenga en cuenta que la sección de extends de nuestro archivo debe mantener la configuración del complemento de Prettier en la última línea para mostrar los errores de Prettier como errores de ESLint visibles en nuestro editor. Estamos siguiendo la configuración recomendada de ESLint para TypeScript, con algunas preferencias personalizadas agregadas en la sección de rules . Siéntase libre de explorar las reglas disponibles y personalizar aún más su configuración. Elegimos incluir:

  • Un error si no usamos cadenas entre comillas simples.
  • Una advertencia cuando no proporcionamos un caso default en las declaraciones de switch .
  • Un aviso si reasignamos algún parámetro de una función.
  • Una advertencia si llamamos a una declaración de await dentro de un bucle.
  • Un error para las variables no utilizadas, que hacen que el código sea ilegible y propenso a errores con el tiempo.

Ya hemos configurado nuestra configuración de ESLint para que funcione con el formato Prettier. (Hay más información disponible en el proyecto GitHub eslint-config-prettier ). Ahora, podemos crear el archivo de configuración de Prettier, .prettierrc :

 { "trailingComma": "none", "tabWidth": 4, "semi": true, "singleQuote": true }

Estas configuraciones son de la documentación oficial de Prettier; puedes modificarlos como desees. Actualizamos las siguientes propiedades:

  • trailingComma : cambiamos esto de es5 a none para evitar las comas finales.
  • semi : cambiamos esto de false a true porque preferimos tener un punto y coma al final de cada línea.

Finalmente, es hora de ver ESLint y Prettier en acción. En nuestro archivo app.ts , cambiaremos el tipo de variable de response de const a let . Usar let no es una buena práctica en este caso ya que no modificamos el valor de la response . El editor debería mostrar un error, la regla incumplida y sugerencias para corregir el código. No olvide habilitar ESLint y Prettier en su editor si aún no están configurados.

El editor muestra una línea de código que asigna un valor a la variable "respuesta let". La línea muestra una bombilla amarilla junto a ella, y la palabra "respuesta" tiene un subrayado rojo y una ventana emergente de error encima. La ventana emergente de error primero define la variable "respuesta" y dice: "respuesta let: { código de estado: número; cuerpo: cadena; }". Debajo de la definición, el mensaje de error dice: "La 'respuesta' nunca se reasigna. Use 'const' en su lugar. eslint (prefer-const)". Debajo del mensaje de error, se leen dos opciones: "Ver problema" o "Solución rápida".

Mantener el código con Jest Testing

Hay muchas bibliotecas disponibles para realizar pruebas, como Jest, Mocha y Storybook. Usaremos Jest en nuestro proyecto por algunas razones:

  • Es rápido de aprender.
  • Requiere una configuración mínima.
  • Ofrece pruebas instantáneas fáciles de usar.

Instalamos las dependencias requeridas:

 npm i --save-dev jest ts-jest @types/jest

A continuación, crearemos un archivo de configuración de Jest, jest.config.js , dentro de la raíz del proyecto:

 module.exports = { roots: ['src'], testMatch: ['**/__tests__/**/*.+(ts|tsx|js)'], transform: { '^.+\\.(ts|tsx)$': 'ts-jest' } };

Estamos personalizando tres opciones en nuestro archivo:

  • roots : esta matriz contiene las carpetas en las que se buscarán archivos de prueba; solo verifica debajo de nuestra subcarpeta src .
  • testMatch : esta matriz de patrones globales incluye las extensiones de archivo que se considerarán archivos Jest.
  • transform : esta opción nos permite escribir nuestras pruebas en TypeScript usando el paquete ts-jest .

Hagamos una nueva carpeta __tests__ dentro src/test-lambda . Dentro de eso, agregaremos el archivo handler.test.ts , donde crearemos nuestra primera prueba:

 import { handler } from '../app'; const event: any = { body: JSON.stringify({}), headers: {} }; describe('Demo test', () => { test('This is the proof of concept that the test works.', async () => { const res = await handler(event); expect(res.statusCode).toBe(200); }); });

Volveremos a nuestro archivo package.json y lo actualizaremos con el script de prueba:

 "test": "jest"

Cuando vamos a la terminal y ejecutamos npm run test , deberíamos recibir una prueba de aprobación:

La parte superior de la consola muestra un indicador verde "Aprobado" y el nombre del archivo de prueba, "src/test-lambda/__tests__/handler.test.ts". La siguiente línea dice: "Prueba de demostración". La siguiente línea muestra una marca de verificación verde seguida de "Esta es la prueba de concepto de que la prueba funciona. (1 ms)". Después de una línea en blanco, la primera línea dice: "Suites de prueba: 1 aprobado, 1 total". El segundo dice: "Exámenes: 1 aprobado, 1 total". El tercero dice: "Instantáneas: 0 en total". El cuarto dice: "Tiempo: 0,959 s". La última línea dice: "Ejecutó todos los conjuntos de pruebas".

Manejar el control de fuente con .gitignore

Deberíamos configurar Git para excluir ciertos archivos del control de código fuente. Podemos crear un archivo .gitignore usando gitignore.io para omitir los archivos que no son necesarios:

 # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Runtime data pids *.pid *.seed *.pid.lock npm-debug.log package.lock.json /node_modules .aws-sam .vscode # TypeScript cache *.tsbuildinfo # Optional npm cache directory .npm # Optional ESLint cache .eslintcache

Preparados, listos, construidos: nuestro modelo para el éxito

Ahora tenemos un proyecto repetitivo de AWS SAM completo con TypeScript. Nos hemos centrado en obtener los conceptos básicos correctos y mantener una alta calidad de código con compatibilidad con ESLint, Prettier y Jest. El ejemplo de este tutorial de AWS SAM puede servir como modelo, encaminando su próximo gran proyecto desde el principio.

El blog de ingeniería de Toptal agradece a Christian Loef por revisar los ejemplos de código presentados en este artículo.

El logotipo de AWS con la palabra "SOCIO" y el texto "Servicios de nivel avanzado" debajo.
Como socio consultor avanzado en Amazon Partner Network (APN), Toptal ofrece a las empresas acceso a expertos certificados por AWS, bajo demanda, en cualquier parte del mundo.