Construyendo un template para tus proyectos de serverless framework y TypeScript

Kenny J. Luque
Pragma
Published in
7 min readMar 27, 2024

Introducción

Cuando se trabaja con el Serverless Framework, una de las decisiones más importantes que enfrentan los desarrolladores es cómo estructurar y organizar sus proyectos. Un buen punto de partida es tener un template sólido que sirva como base para futuros proyectos serverless. En este contexto, construir un template para tus proyectos en Serverless Framework utilizando TypeScript ofrece numerosas ventajas.

En esta guía, exploraremos cómo construir un template para proyectos de Serverless Framework utilizando TypeScript. Cubriremos los fundamentos de la estructura del proyecto, la configuración inicial, la implementación de funciones lambda, la gestión de dependencias y la integración con otros servicios en la nube. Al finalizar, tendrás una sólida base sobre la cual construir y escalar tus propias aplicaciones serverless en TypeScript utilizando el Serverless Framework.

Configuraciones

Eslint

Para este caso usaremos @typescript-eslint/eslint-plugin que es un complemento de Eslint para configurar reglas al usar TypeScript.

Para el formateo y detección de errores usaremos @stylistic/eslint-plugin-ts

Para este caso usaremos la configuración manual y tener el control de lo que necesitamos.

Puedes indagar acerca del comando npm init @eslint/config

Ejecutamos el siguiente comando para instalar los paquetes requeridos

pnpm add --save-dev eslint typescript @typescript-eslint/eslint-plugin @stylistic/eslint-plugin-ts @typescript-eslint/parser

Ahora configuremos eslint desde el archivo .eslintrc.json

{
"env": {
"node": true,
"es2021": true
},
"extends": [],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@stylistic/eslint-plugin-ts",
"@typescript-eslint/eslint-plugin"
],
"rules": {
"@stylistic/ts/semi": [
"error",
"never"
],
"@stylistic/ts/indent": [
"error",
2
],
"@stylistic/ts/quotes": [
"error",
"single"
],
"@stylistic/ts/comma-spacing": [
"error",
{
"before": false,
"after": true
}
],
"@stylistic/ts/object-curly-spacing": [
"error",
"always"
],
"@stylistic/ts/keyword-spacing": [
"error",
{
"after": true,
"before": true
}
],
"@stylistic/ts/space-before-function-paren": [
"error",
{
"anonymous": "always",
"named": "always",
"asyncArrow": "always"
}
],
"@stylistic/ts/space-infix-ops": "error",
"@stylistic/ts/space-before-blocks": [
"error",
{
"functions": "always",
"keywords": "always",
"classes": "always"
}
],
"@stylistic/ts/no-extra-parens": [
"error",
"all",
{
"returnAssign": true,
"enforceForArrowConditionals": true
}
]
}
}

En esta configuración se listan reglas específicas, puede consultar sobre reglas definidas o estándar https://typescript-eslint.io/users/configs/

Agreguemos el archivo .eslintignore

node_modules
.build
.husky
.serverless
.vscode
coverage
.eslintrc.json
serverless-checkov

Usemos una herramienta para generar nuestro archivo .gitignore
https://www.toptal.com/developers/gitignore

Ejecutemos los comandos para ejecutar eslint y corregir errores

pnpx eslint . --ext .ts
detecta errores con las reglas configuradas
pnpx eslint . --ext .ts --fix

Existen alternativas como Prettier, sin embargo Eslint brinda mayores beneficios, puedes revisar los detalles en el siguiente articulo
https://antfu.me/posts/why-not-prettier

Ahora configuremos jest usando ts-jest:

https://www.npmjs.com/package/ts-jest

pnpm i -D jest ts-jest @types/jest

y ejecutamos el comando para iniciar la configuración

pnpx ts-jest config:init

esto nos genera un archivo jest.config.js, pero para mantener el formato de configuración y simplificar el admitir js y ts en el mismo proyecto, lo cambiaré por la extensión .json

{
"preset": "ts-jest",
"testEnvironment": "node",
"displayName": "nombre de tu repositorio/stack",
"roots": ["<rootDir>"],
"modulePaths": ["."],
"moduleNameMapper": {
},
"setupFilesAfterEnv": [
"./jest.setup.ts"
],
"moduleFileExtensions": [
"js",
"mjs",
"cjs",
"jsx",
"ts",
"tsx",
"json",
"node",
"d.ts"
],
"testPathIgnorePatterns": [
"node_modules",
".build",
"coverage",
".esbuild"
],
"modulePathIgnorePatterns": [
"node_modules",
".build",
"coverage",
".esbuild",
"<rootDir>/.build",
".serverless"
],
"verbose": true,
"collectCoverage": true,
"coverageDirectory": "coverage",
"collectCoverageFrom": [
"**/src/**/*.{js,jsx,ts,tsx}",
"!**/node_modules/**",
"!**/coverage/**"
],
"coverageProvider": "babel",
"coverageThreshold": {
"global": {
"branches": 100,
"functions": 100,
"lines": 100,
"statements": 100
}
},
"coveragePathIgnorePatterns": [
"node_modules",
".build/*",
"coverage",
".esbuild",
".serverless"
]
}

Aquí existe algo importante y es el archivo jest.setup.ts
el cual contiene una configuraciones adicionales el cual puedes agregar a medida, para este caso agregaremos el lector de variables de entorno usando dotenv y agregaremos el tipado para node

pnpm add -D dotenv @types/node

y el archivo jest.setup.ts se veria así:

import 'dotenv/config'
require('dotenv').config({ path: require('path').resolve(__dirname, './test/.env.test') })

Configuremos el archivo tsconfig.json, con las opciones deseadas para el compilador

{
"compilerOptions": {
"lib": [
"ESNext",
"DOM"
],
"module": "CommonJS",
"target": "ESNext",
"strict": true,
"strictNullChecks": true,
"esModuleInterop": true,
"moduleResolution": "node",
"outDir": ".build",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"rootDir": ".",
"baseUrl": ".",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": true,
"noUnusedParameters": false,
"strictPropertyInitialization": false,
"noFallthroughCasesInSwitch": true,
},
"include": [
"./src/**/*.ts",
"./test/**/*.ts"
],
"exclude": [
"node_modules",
".vscode/**/*",
".build/**/*",
"./dist"
]
}

Veamos como realizar nuestro primer test usando TDD, crearemos uno dentro de test/

Usaremos algunos mocks para emular nuestro llamado a handler y al ejecutar vemos que falla, este es el primer paso de TDD, luego de ello nos guiamos para desarrollar basados en los test

import { handler } from 'src/functions/hello'

describe('Handler should be return a valid response', () => {
test('It should return a valid response', async () => {
// Arrange
const event = ''
const context = ''
// Act
const response = await handler(event, context)
// Assert
expect(response).toBe('')
})
})
jest --ci --detectOpenHandles --config=jest.config.json

Ahora modificamos nuestro test, añadiendo mocks, tipando la respuesta y la aserción

import { handler } from 'src/functions/hello'
import { eventGenerator } from './__mocks__/request/eventGenerator'
import { contextMock } from './__mocks__/request/httpMock'
import { APIGatewayProxyStructuredResultV2 } from 'aws-lambda'

describe('Handler should be return a valid response', () => {
test('It should return a valid response', async () => {
// Arrange
const event = eventGenerator({ })
const context = contextMock
// Act
const response: APIGatewayProxyStructuredResultV2 = await handler(event, context)
// Assert
expect(response.statusCode).toBe(200)
})
})
Por defecto jest lanza los test en paralelo

Para generar los mocks lo tomamos de los registros de AWS CloudWatch añadiendo console.log(JSON.stringify(event), JSON.stringify(context))

y nuestra función queda así:

import { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2, Context } from 'aws-lambda'

export const handler = async (event: APIGatewayProxyEventV2, context: Partial<Context>): Promise<APIGatewayProxyStructuredResultV2> => {

return {
body: JSON.stringify({
data: 'Hola mundo desde serverless framework',
}),
statusCode: 200,
}
}

Me gustaría poder automatizar la ejecución del linter, la ejecución de pruebas y que mis commits tenga una estructura estándar, para ello usaremos Husky https://typicode.github.io/husky/, nos ofrece personalizar acciones para git hooks. Simplemente ejecutamos el siguiente comando:

pnpm add --save-dev husky && pnpm exec husky init

adicionalmente usaremos Lint-Staged, nos permite ejecutar scripts en los archivos que se encuentran en stagging, es decir, solo en los archivos que hemos modificado, no en todos los archivos del proyecto. Se entiende que el resto de archivos que no hemos tocado ya están bien y no necesitan ser revisados. Creamos un archivo .lintstagedrc

{
"**/*.{js,jsx,ts,tsx}": [
"eslint",
"eslint --fix"
]
}

Ahora necesitamos una herramienta para validar el uso de conventional commits, usaremos commitlint.

pnpm install -D @commitlint/config-conventional @commitlint/cli @commitlint/types

Creamos un archivo commitlint.config.ts

import type { UserConfig } from '@commitlint/types' 
import { RuleConfigSeverity } from '@commitlint/types'

const Configuration: UserConfig = {

extends: ['@commitlint/config-conventional'],
parserPreset: 'conventional-changelog-conventionalcommits',
formatter: '@commitlint/format',
rules: {
'type-enum': [
RuleConfigSeverity.Error,
'always',
[
'feat', 'fix', 'docs', 'style', 'refactor',
'test', 'revert', 'content', 'chore', 'build', 'ci',
'perf', 'wip', 'release', 'merge', 'config', 'deploy',
'security', 'dependency'
]
],
'subject-case': [RuleConfigSeverity.Error, 'always', 'sentence-case'],
}
}

export default Configuration

Y finalmente configuramos las acciones para cada git hook

npx --no -- commitlint --edit $1

npx lint-staged

npm run test

Listo, ahora subamos nuestros cambios, usando
git commit -m “skeleton ts serverless framework!”

Esto nos obliga a seguir las convenciones según el estándar, intentemos nuevamente con la siguiente estructura:

git commit -m "feat(skeleton): Create a skeleton for serverless framework using ts"

Ahora al hacer push, automáticamente se ejecutaran las pruebas unitarias, si todo sale bien se realiza la subida al origen remoto.

Conclusión:

Hemos revisado cada una de las herramientas y las configuramos para que se acoplen facilitando el desarrollo, la consistencia y el desarrollo de código limpio; con las acciones de verificación para eventos de git hook
garantizamos que lo anterior se cumpla, facilitando la colaboración e integración continua garantizando que el código enviado cumple con los estándares de validación estática, así mismo brinda una convención para un versionado organizado para el seguimiento de cambios.

Por último les comparto el repositorio de esta serie de blogs.

https://github.com/Kenny2397/serverless-framework-series

Referencias

https://typicode.github.io/husky/
https://git-scm.com/docs/githooks
https://kulshekhar.github.io/ts-jest/

https://www.conventionalcommits.org/en/v1.0.0/
https://git-scm.com/docs/githooks
https://eslint.style/guide/migration
https://commitlint.js.org/
https://typescript-eslint.io/
https://antfu.me/posts/why-not-prettier

Kenny J. Luque @kennyluquet
Mira mis otros artículos aquí

--

--

Kenny J. Luque
Pragma
0 Followers
Writer for

🚀 Serverless enthusiast