# Creación de servicio BackEnd Un servicio BackEnd será un servicio independiente con labores específicas que se ejecuta en el servidor, su nombre siempre debe comenzar con la palabra `api`. Puntualmente: - **api:** Interacción con Base de datos y orquestación del resto de servicios - **api-files:** Manipulación de archivos, obtención de propiedades (tipo, extensión, peso, etc.), alojamiento en destino (S3, AWS, directorio de servidor, etc.) - **api-pdf:** Manipulación de archivos PDF, lectura, generación, modificación, etc. - **api-email:** Gestión de correo electrónico, cuentas de correo y envío. - **api-webhook-[type]:** Servicio residente que permanece atento a respuestas de terceros (transacciones monetarias, firma realizada, validación OAuth, etc.). En este caso `type` vendría a ser un sufijo para especificar el tipo de WebHook en caso de haber más de uno dentro del proyecto completo. > **NOTA:** > Considerar que siempre un proyecto de este tipo debe estar dentro del proyecto principal. ### Creación del directorio Para crear un nuevo proyecto, se debe abrir una terminal y ejecutar los siguientes comandos: ```ssh mkdir <<nombre proyecto>> cd <<nombre proyecto>> yarn init -y ``` ### TypeScript Como el servicio será programado en TypeScript, necesitaremos instalar lo siguiente: ```ssh yarn add typescript @types/node ts-node-dev --dev ``` Luego necesitaremos inicializar el archivo de configuración: ```ssh npx tsc --init ``` Este archivo de configuración se generará en la raíz del proyecto con el nombre `tsconfig.json` y servirá para la compilación del servicio y debe tener el siguiente contenido: ```json { "compilerOptions": { "target": "es2016", "module": "commonjs", "outDir": "./dist", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true } } ``` > **NOTA:** > El contenido del archivo se visualizará mucho más grande, pero en su mayoría estará comentado. Por ello es preferible borrar ese contenido y reemplazarlo por lo que acá se indica. ### Instalación de librerías necesarias Posteriormente se deben instalar las librerías mínimas para desarrollar: ```ssh yarn add cors dotenv express @hapi/boom joi pg pg-promise uuid winston helmet multer bcryptjs jsonwebtoken ``` Luego sus respectivos complementos para TypeScript: ```ssh yarn add @types/cors @types/express @types/pg @types/joi @types/multer @types/pg @types/bcryptjs @types/jsonwebtoken nodemon --dev ``` ### Variables de entorno Lo siguiente es crear un archivo de variables de entorno, este debe llamarse `.env` dentro de él deben establecerse los valores de estas, algunas pueden ser, por ejemplo, el puerto de ejecución de la aplicación y un api-key: - `API_NAME` Nombre del servicio - `API_PORT` Puerto de la api - `API_KEY` Key de la api También se puede establecer el entorno de trabajo - `NODE_ENV` production | develop > **NOTA:** > Conforme se expliquen más adelante otros componentes de esta aplicación, se detallará qué otras variables de entorno deben agregarse a este archivo. ### Directorios y archivos que se deben ignorar en GitHub Es importante también contemplar el archivo `.gitignore` para que no quede en el repositorio de GitHub los directorios propios de las librerías que se descargan o el mismo archivo de variables de entorno: ```ssh # dependencies /node_modules node_modules/ /.pnp .pnp.js # production /build /dist # debug npm-debug.log* yarn-debug.log* yarn-error.log* yarn.lock .pnpm-debug.log* .yarn # local env files .env .env.test .env*.local # Log log *.log ``` ### Configuración scripts de ejecución Se configurarán scripts de ejecución en el archivo `package.json` en una sección llamada `scripts` con el objetivo de abreviar los comandos: - `yarn dev` para ejecutar la aplicación en modo developer (TypeScript) - `yarn start` para ejecutar la aplicación en modo productivo (JavaScript) - `yarn build` para transpilar la aplicación (transformar desde TypeScript a JavaScript) Para esto se debe agregar la sección `script` al archivo `package.json` como sigue: ```json { "name": "api", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build": "tsc", "dev": "nodemon src/server.ts", "start": "node dist/server.js" }, "keywords": [], "author": "", ... } ``` ### Configuración del contenedor Normalmente los servicios son llevados a contenedores para hacerlos transportables y ampliamente compatibles aprovechando las opoetunidades que brinda la tecnología Dockers, por ello debe existir un archivo de configuración para convertir el servicio en una imagen y posteriormente en un contenedor. El archvo de debe llamar `Dockerfile` y debe contener el siguiente código: ```typescript FROM node:20-alpine RUN apk add --no-cache tzdata ENV TZ=America/Santiago # Directorio de trabajo en el contenedor WORKDIR /app # Copiar los archivos de la aplicación COPY package.json yarn.lock ./ COPY .env /app/dist/ # Instalar las dependencias y construir la aplicación RUN yarn install COPY . . RUN yarn build # Directorio de trabajo en el contenedor WORKDIR /app/dist # Exponer el puerto en el que se ejecuta la aplicación EXPOSE <port> # Comando para iniciar la aplicación CMD ["node", "server.js"] ``` > **NOTA:** > Considerar que se debe cambiar `<port>` por el valor del puerto incluido en las variables de entorno. ### Directorios Luego dentro del directorio del servicio, debe existir un directorio `src` y dentro de él se deben crear los siguientes subdirectorios: - `data` Para archivos con datos estáticos - `interfaces` Interfaces de las estructuras - `middlewares` Funciones que se ejecutan entre peticiones - `routers` Enrutamiento - `controllers` Controladores - `models` Modelos que interactúan con la base de datos - `queries` Queries SQL - `schemas` Esquemas de validación - `utils` Funciones de utilidad general ### Interfaces Dentro del directorio `/src/interfaces`, debe al menos existir un archivo `src/interfaces/response.ts` donde se especifica una interface genérica para las respuestas que se deplegará al usuario, este debe tener el siguiente código: ```typescript export interface UpdateResponse { success: boolean; data: JSON | null; error: JSON | string | null; } ``` ### Funciones generales obligatorias Dentro del directorio `/src/utils` deben existir algunas funciones mínimas obligatorias que serán muy útiles y complementarias para el servicio. 1. **Configuración** Debe existir un archivo de configuración llamado `src/utils/config.ts` que debe contener un código como el que sigue (dependiendo obviamente de las variables de entorno del proyecto): ```typescript import cnf from "dotenv"; cnf.config(); const config = { apiName: process.env.API_NAME || "api", apiPort: process.env.API_PORT || 3001, apiKey: process.env.API_KEY || "1234", ... }; export default config; ``` 2. **Log** Para realizar registro de las actividades que realiza el servicio, es necesaria la creación de archivos log donde se detalla no solo la accion sino también datos anexos y complementarios como la hora, ruta, fecha, parámetros, etc. El nombre del archivo debe ser `src/utils/logger.ts` y contener el siguiente código: ```typescript= // src/utils/logger.ts import { createLogger, format, transports } from "winston"; import config from "./config"; const datetoString = () => { const offset = new Date().getTimezoneOffset(); const yourDate = new Date(new Date().getTime() + offset * 60 * 1000); return yourDate.toISOString().split("T")[0]; }; export default createLogger({ format: format.combine( format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), format.json() ), transports: [ new transports.File({ maxsize: 512000, filename: `${__dirname}/../../../logs/log-${ config.apiName }-${datetoString()}.log`, }), new transports.Console({ level: "debug" }), ], }); ``` **Variables de entorno** Se debe configurar las variables de entorno en `src/utils/config.ts` las que obtendrán su valor desde el archivo de variables de entorno `.env` La variable de entorno es la siguiente: ```ssh API_NAME=api ``` 3. **DataBase** Si el servicio interactúa con la base de datos, entonces, es necesario que cuente con esta rutina de conexión. El nombre del archivo debe ser `src/utils/database.ts` y contener el siguiente código: ```typescript= // src/utils/database.ts import pg from "pg"; import path from "path"; import fs from "fs"; import config from "./config"; import createLogger from "./logger"; const { Pool } = pg; const { dbHost, dbUser, dbPassword, dbName, dbPort } = config; const pool = new Pool({ user: dbUser, host: dbHost, database: dbName, password: dbPassword, port: dbPort, keepAlive: true, max: 20, options: "-c timezone=America/Santiago", }); pool.connect(function (err) { if (err) { createLogger.error({ util: "database", error: err, }); return; } createLogger.info({ util: "database", message: "Database connected", }); }); export default pool; ``` > **NOTA:** > Puntualmente este código es para conectividad al motor Postgres **Variables de entorno** Se debe configurar las variables de entorno en `/utils/config.ts` las que obtendrán su valor desde el archivo de variables de entorno `.env` Las variables de entorno para acceso a la base de datos, son las siguientes: - `DB_HOST` IP Servidor de base de datos - `DB_NAME` Nombre de la base de datos - `DB_USER` Usuario base de datos - `DB_PASSWORD` Contraseña del usuario de base de datos - `DB_PORT` Puerto de la base de datos Archivo `/utils/config.ts`: ```javascript import cnf from "dotenv"; cnf.config(); const config = { ... dbHost: process.env.DB_HOST || "localhost", dbName: process.env.DB_NAME || "postgres", dbUser: process.env.DB_USER || "postgres", dbPassword: process.env.DB_PASSWORD || "password", dbPort: parseInt(process.env.DB_PORT || "5432"), ... }; export default config; ``` Archivo `.env`: ```ssh APP_NAME=api ``` 4. **Respuesta desde controlador** Con el objetivo de estandarizar el formato de la respuesta del servicio, es que se crea la función `sendResponse` que está contenida dentro del archivo `src/utils/sendResponse.ts` y debe contener el siguiente código: ```typescript import { Request, Response, NextFunction } from "express"; import createLogger from "./logger"; const sendResponse = ( req: Request, res: Response, json: any, statusCode = 200, error = null ) => { createLogger.info({ url: req.originalUrl, method: req.method, body: req.method === "POST" ? req.body : "", params: req.method !== "POST" ? req.params : "", query: req.method === "GET" ? req.query : "", }); res.status(statusCode).json({ success: error ? false : true, data: json, error, }); }; export default sendResponse; ``` Esto quiere decir que toda respuesta de este servicio tendrá el formato: ```json { success: boolean, data: json | null, error: string | json | null } ``` ### Middlewares obligatorios Dentro del directorio `/src/middlewares`, deben existir 4 middlewares que son obligatorios ya que vienen a ser funcionalidades mínimas de seguridad o monitoreo del servicio. 1. **Auth** Para brindar una capa inicial de seguridad, se establece una `api-key`, la cual se declara en el archivo `.env` con un valor que es verificado en este middleware que debe llamarse `src/middlewares/auth.ts` y contener el siguiente código: ```typescript import config from "../utils/config"; import { NextFunction, Request, Response } from "express"; const auth = (req: Request, res: Response, next: NextFunction) => { const { apiKey } = config; if (req.headers.id !== apiKey) { res.status(401).json({ message: "Incorrect api key" }); return; } return next(); }; export default auth; ``` 2. **Origenes permitidos** Se trata de un arreglo con las URL de origen a las cuales el servicio les podrá responder, esta es una seguridad a nivel de CORS, el nombre del archivo debe ser `src/middlewares/allowedOrigins.ts` y contener el siguiente código según sean los origenes aceptados: ```typescript export const allowedOrigins = [ "http://localhost:3001", ]; ``` 3. **Logger** Para poder dejar registro log de los eventos que ocurren dentro de este servicio, debemos contar on una función que realice este registro como middleware, el nombre del archivo debe ser `src/middlewares/logger.ts` y contener el siguiente código: ```typescript import createLogger from "../utils/logger"; const reqLogger = (req: any, res: any, next: any) => { createLogger.info({ url: req.originalUrl, method: req.method, body: req.method === "POST" ? req.body : "", params: req.method !== "POST" ? req.params : "", query: req.method === "GET" ? req.query : "", }); return next(); }; const resLogger = (req: any, res: any, next: any) => { return next(); }; export { reqLogger, resLogger }; ``` 4. **Security Headers** Para establecer un nivel de seguridad a nivel de headers, es que se incluye este middleware, el nombre del archivo debe ser `src/middlewares/setSecurityHeaders.ts` y contener el siguiente código: ```typescript import { Request, Response, NextFunction } from "express"; export function setSecurityHeaders( req: Request, res: Response, next: NextFunction ) { res.setHeader( "Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload" ); res.setHeader("X-Frame-Options", "DENY"); res.setHeader("X-Content-Type-Options", "nosniff"); res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin"); next(); } ``` 5. **Handler Request** Para dejar registro en log de la utilización del servicio es que utiliza esta middleware, el nombre del archivo debe ser `src/middlewares/handlerRequest.ts` y debe contener el siguiente código: ```javascript import { Request, Response, NextFunction } from "express"; import createLogger from "../utils/logger"; const handlerRequest = ( req: Request, res: Response, next: NextFunction ): void => { createLogger.info({ url: req.originalUrl, method: req.method, body: req.method === "POST" ? req.body : "", params: req.method !== "POST" ? req.params : "", query: req.method === "GET" ? req.query : "", }); return next(); }; export default handlerRequest; ``` 6. **Handler Error** Para dejar registro en log de los errores que pueden generarse mientras se ejecuta el servicio es que utiliza esta middleware, el nombre del archivo debe ser `src/middlewares/handlerError.ts` y debe contener el siguiente código: ```typescript import { Request, Response, NextFunction } from "express"; import boom, { Boom } from "@hapi/boom"; import createLogger from "../utils/logger"; const handlerError = ( err: Boom, req: Request, res: Response, next: NextFunction ) => { if (err) { const wrapperError = err.isBoom ? err : boom.badImplementation(err); createLogger.error({ url: req.originalUrl, method: req.method, body: req.method === "POST" ? req.body : "", params: req.method !== "POST" ? req.params : "", query: req.method === "GET" ? req.query : "", error: err.message, }); return res.status(wrapperError.output.statusCode).json({ success: false, data: null, error: wrapperError.output.payload.message, }); } next(); }; export default handlerError; ``` 7. **Handler Response** Para dejar registro en log de las rutas no existentes que pueden ser solicitadas mientras se ejecuta el servicio es que utiliza esta middleware, el nombre del archivo debe ser `src/middlewares/handlerResponse.ts` y debe contener el siguiente código: ```typescript import { Request, Response, NextFunction } from "express"; import createLogger from "../utils/logger"; const handlerResponse = ( req: Request, res: Response, next: NextFunction ): void => { createLogger.error({ url: req.originalUrl, method: req.method, body: req.method === "POST" ? req.body : "", params: req.method !== "POST" ? req.params : "", query: req.method === "GET" ? req.query : "", error: "Not found", }); res.json({ success: false, data: null, error: { status: 404, message: "not found", }, }); return next(); }; export default handlerResponse; ``` 8. **Subida de archivos** Para brindar la posibilidad de subir archivo, el nombre del archivo debe ser `src/middlewares/multer.ts` y debe contener el siguiente código: ```typescript= // src/middlewares/multer.ts import multer from "multer"; const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, "../uploads/"); }, filename: (req, file, cb) => { cb(null, file.fieldname + "-" + Date.now() + file.originalname); }, }); export const upload = multer({ storage: storage }); ``` ### Archivos principales del servicio Existen dos archivos que son fundamentales para el funcionamiento del servicio, estos se encuentran en la raíz de este `/src`. 1. **Aplicación** Este archivo contiene toda la configuración, seguridad y orquestación de rutas, monitoreo y accesos, el nombre del archivo debe ser `src/app.ts` y contener el siguiente código: ```typescript import express, { Express, Request, Response, NextFunction } from "express"; import cors from "cors"; import path from "path"; import helmet from "helmet"; import { setSecurityHeaders } from "./middlewares/setSecurityHeaders"; import { allowedOrigins } from "./middlewares/allowedOrigins"; import handlerResponse from "./middlewares/handlerResponse"; import handlerRequest from "./middlewares/handlerRequest"; import handlerError from "./middlewares/handlerError"; import createLogger from "./utils/logger"; import * as routers from "./routers"; const corsOptions = { preflightContinue: false, methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], origin: ( origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void ) => { if (process.env.ENV !== "dev") { if (!origin || allowedOrigins.indexOf(origin) !== -1) { callback(null, true); } else { createLogger.error({ controller: "CORS", error: "Not allowed by CORS, origin: " + origin, }); callback(null, false); } } else { callback(null, true); } }, credentials: true, }; const routerMappings = [ { path: "/post", router: routers.PostRouter }, ]; function initializeRoutes(server: Express) { routerMappings.forEach((router) => { server.use(router.path, router.router, handlerError); }); server.use((err: any, req: Request, res: Response, next: NextFunction) => { if (err instanceof SyntaxError && err.message.includes("JSON")) { return res.status(400).json({ error: "Json Request Format is invalid" }); } return res.status(500).json({ error: "Internal server error" }); }); const virtualPath = "/<<virtualPath>>"; const diskPath = path.join(__dirname, "..", "<<diskPath>>"); server.use(virtualPath, express.static(diskPath)); } const server = express(); server.use(setSecurityHeaders); server.use(express.json()); server.use(cors(corsOptions)); server.use(express.urlencoded({ extended: false })); server.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], }, }, referrerPolicy: { policy: "strict-origin-when-cross-origin" }, frameguard: { action: "sameorigin" }, xssFilter: true, noSniff: true, hsts: { maxAge: 31536000, includeSubDomains: true }, }) ); server.use(handlerRequest); initializeRoutes(server); server.use(handlerError); server.use(handlerResponse); export default server; ``` 2. **Servidor** Este archivo es el que inicializa el servicio y brinda el número de puerto previamente configurado en las variables de entorno. El nombre del archivo debe ser `src/server.ts` y contener el siguiente código: ```typescript import config from "./utils/config"; import createLogger from "./utils/logger"; import app from "./app"; const { apiPort } = config; app.listen(apiPort); createLogger.info(`API listening port ${apiPort}`); ``` ### Resultado final Para verificar que todo está correcto, el proyecto debe tener la siguiente estructura mínima de directorios y archivos: ```ssh /[main-app] /api-[name] /src /data /interfaces /middlewares allowedOrigins.ts auth.ts handlerResponse.ts handlerRequest.ts handlerError.ts logger.ts multer.ts setSecurityHeaders.ts /routes /controllers /models /queries /schemas /utils config.ts database.ts logger.ts sendResponse.ts app.ts server.ts .env .gitignore Dockerfile package.json tsconfig.json ``` Ver los siguientes documentos: - [**Routes**](https://hackmd.io/@mGoZaVHZQTWgwFlq-J3U0w/B1zN9PAPp)<br> - [**Controllers**](https://hackmd.io/@mGoZaVHZQTWgwFlq-J3U0w/SyFI9PAvT)<br> - [**Models**](https://hackmd.io/@mGoZaVHZQTWgwFlq-J3U0w/Hy5jcPCvp)<br> - [**Queries**](https://hackmd.io/@mGoZaVHZQTWgwFlq-J3U0w/B1KJswCv6)<br> - [**Schemas**](https://hackmd.io/@mGoZaVHZQTWgwFlq-J3U0w/Syhv28zk0)<br>