En el emocionante mundo del desarrollo web, JavaScript ha sido el lenguaje de programación dominante durante mucho tiempo. Su flexibilidad y facilidad de uso lo han convertido en la elección favorita de los desarrolladores para crear aplicaciones web interactivas y dinámicas. Sin embargo, a medida que las aplicaciones web se vuelven cada vez más complejas, también aumentan los desafíos asociados con el mantenimiento y la escalabilidad del código.
Aquí es donde entra TypeScript, un superconjunto de JavaScript que agrega características de tipado estático y características adicionales al lenguaje. Desarrollado por Microsoft, TypeScript ha ganado popularidad en los últimos años debido a su capacidad para mejorar la seguridad y la productividad en el desarrollo de aplicaciones JavaScript. En este artículo, exploraremos qué es TypeScript y cómo puede beneficiar a los desarrolladores.
¿Qué es TypeScript?
TypeScript se describe a menudo como un “JavaScript tipado estáticamente”. A diferencia de JavaScript, donde los tipos de variables son inferidos de forma dinámica en tiempo de ejecución, TypeScript permite a los desarrolladores agregar anotaciones de tipos estáticos a sus variables, funciones y objetos. Estas anotaciones de tipo proporcionan información adicional al compilador de TypeScript, lo que permite detectar errores y problemas potenciales durante la fase de compilación en lugar de en tiempo de ejecución.
Beneficios de TypeScript:
-
Mayor seguridad: Al agregar tipos estáticos, TypeScript ayuda a detectar errores comunes, como operaciones no válidas en tiempo de compilación, uso incorrecto de variables y llamadas a funciones inexistentes. Esto reduce significativamente la posibilidad de errores en tiempo de ejecución y facilita el mantenimiento del código.
-
Mejor productividad: TypeScript ofrece herramientas de desarrollo avanzadas, como autocompletado de código, navegación rápida entre archivos y detección temprana de errores. Estas características mejoran la productividad de los desarrolladores al proporcionar sugerencias contextuales y advertencias en tiempo real, lo que les permite escribir código de manera más eficiente y rápida.
-
Escalabilidad: A medida que las aplicaciones web crecen en tamaño y complejidad, mantener un código bien organizado y escalable se vuelve crucial. TypeScript ofrece características orientadas a objetos y módulos, lo que facilita la estructuración y el mantenimiento de proyectos a gran escala. Además, al permitir la verificación de tipos, TypeScript facilita la refactorización del código y la incorporación de nuevas funcionalidades sin temor a romper el código existente.
-
Compatibilidad con JavaScript: TypeScript es un superconjunto de JavaScript, lo que significa que cualquier código JavaScript existente es válido en TypeScript. Esto facilita la adopción gradual de TypeScript en proyectos existentes sin la necesidad de reescribir el código por completo. Los desarrolladores pueden comenzar a utilizar TypeScript en partes específicas del proyecto y ampliar gradualmente su uso según sea necesario.
Creando un scaffold de una api en Typescript
En este proyecto, nos sumergiremos en la construcción de un scaffold para una API utilizando TypeScript y Express. A medida que las aplicaciones web continúan evolucionando, las API se han convertido en la columna vertebral de muchas soluciones modernas, permitiendo la comunicación y el intercambio de datos entre diferentes sistemas de manera eficiente y segura.
Comenzaremos por familiarizarnos con TypeScript, un superconjunto de JavaScript que agrega características de tipado estático al lenguaje. Exploraremos cómo utilizar TypeScript para mejorar la seguridad y la confiabilidad en el desarrollo de nuestra API, al permitirnos detectar errores en tiempo de compilación y mantener un código más robusto.
1. Crear proyecto en GIT
2. Crear y configurar .gitignore
.idea
.vscode
**/node_modules
dist
package-lock.json
3. Instalar typescript y nodemon como dependencias “dev”
npm i nodemon --save-dev
npm i typescript --save-dev
4. Configuramos Typescript
Inicializamos Typescript
npx tsc --init
Agregamos la siguiente configuración al archivo creado
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"inlineSourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"strictPropertyInitialization": false
},
"include": [
"app/**/*.ts"
],
"exclude": [
"node_modules",
"test/**/*.ts"
]
}
5. Instalación de express y algunos types necesarios
Para la instalación de express requeriremos los siguientes paquetes.
- express
Winston es un middleware que nos ayudara con los logs de nuestra aplicación
- winston
- expressWinston
Helmet es recomendado por express como buena practica de seguridad para la validación de headers puedes verlo en: https://expressjs.com/en/advanced/best-practice-security.html#use-helmet
- helmet
- body-parser
npm install express winston express-winston helmet body-parser --save
También instalaremos los types de dichos paquetes para poder utilizarlos con typescript
npm install @types/express @types/body-parser --save-dev
6. Antes de configurar express necesitaremos un controlador sencillo.
Crearemos la carpeta donde se encontrara todo nuestro proyecto llamada “app”
Creamos nuestro controlador que sera un controlador “Health” unicamente nos devolverá estatus de nuestra API en este caso unicamente nos devolverá que versión estamos utilizando de express. El controlador se encontrara en la carpeta “app/controllers” y se llamara “health.controller.ts” y el cual contendrá lo siguiente.
import express from "express";
const HTTP_STATUS_CODE_OK = 200;
const EXPRESS_VERSION = '4.17.17';
export class HealthController {
/**
* Get health of application
* @param req
* @param res
*/
async get(req: express.Request, res: express.Response) {
return res.status(HTTP_STATUS_CODE_OK).send({
expressVersion: EXPRESS_VERSION
})
}
}
7. Necesitaremos un router para poder administrar las rutas de nuestros recursos o módulos.
Crearemos una carpeta llamada “app/routes/interfaces” y crearemos una interfaz que nos defina 2 funcionalidades principales. Esta interfaz debe configurar sus rutas, es decir, realizar el registro de cada una de las rutas para el recurso indicado, añadiendo los middlewares e indicando el handler y obtener el nombre del router.
En este caso, no es necesaria la segregación de interfaces, debido a que en teoría no habría un router sin nombre o sin rutas. Entonces, el nombre del archivo sería el siguiente: “router.interface.ts” y contendría el código que se muestra a continuación.
export interface Router {
/**
* Get name of router
* @return string
*/
get name(): string;
/**
* Configure routes for some controller
*/
configureRoutes(): void;
}
8. Crearemos una clase abstracta de un router
Dentro de “app/routes” llamada “abstract.router.ts” que siempre devolverá el nombre del router siguiendo el principio Abierto/Cerrado.
import {Router} from "./interfaces/router.interface";
export abstract class AbstractRouter implements Router {
/**
* RouterName
* @private
*/
protected routerName: string;
configureRoutes(): void {
console.warn("It hasn't routes yet");
}
get name(): string {
return this.routerName;
}
}
9. Es momento de crear el router para nuestro “Health”
Dentro de “app/routes”. El archivo se llamará “health.router.ts”.
import express from "express";
import {HealthController} from "../controllers/health.controller";
import {Router} from "./interfaces/router.interface";
import {AbstractRouter} from "./abstract.router";
export class HealthRouter extends AbstractRouter implements Router {
/**
* @inheritDoc
*/
protected routerName: string = "HealthRouter";
/**
* @inheritDoc
*/
constructor(public app: express.Router) {
super();
this.configureRoutes();
}
/**
* @inheritDoc
*/
configureRoutes() {
let healthController = new HealthController();
this.app.get(`/health`, [
healthController.get,
]);
}
}
10. Crearemos un archivo que obtenga nuestras rutas
Por último, para poder leer cada uno de estos routers y las agregarlos al router de Express. Crearemos un archivo que estará dentro de “app/routes” y se llamará “index.ts”.
import express, {Router as RouterExpress} from "express";
import {Router} from "./interfaces/router.interface";
import {HealthRouter} from "./health.router";
/**
* Configure all routes of API
* @param app
*/
export function setupRoutes(app: express.Application) {
const routes: any = [];
let router = RouterExpress();
routes.push(new HealthRouter(router));
app.use('/api/v1', router)
routes.forEach((route: Router) => {
console.log(`Routes configured for ${route.name}`);
});
}
11. Es momento de crear la configuración de Express.
En dicha configuración agregaremos Winston y Helmet, así como Body Parser. Crearemos el archivo “server.config.ts” dentro de “app/config” con el siguiente código:
import http from "http";
import * as bodyParser from "body-parser";
import * as expressWinston from "express-winston";
import express from "express";
import * as winston from "winston";
import {setupRoutes} from "../routes";
import helmet from "helmet";
/**
* Options for wingston
*/
function getOptionsLogger() {
return {
transports: [
new winston.transports.Console()
],
format: winston.format.combine(
winston.format.colorize(),
winston.format.json()
)
};
}
/**
* Whitelist logger
* @param app
*/
function setMiddlewareLogger(app: express.Application) {
let index = expressWinston.requestWhitelist.indexOf('headers');
if (index !== -1) expressWinston.requestWhitelist.splice(index, 1);
app.use(expressWinston.logger(getOptionsLogger()));
}
/**
* Error logger
* @param app
*/
function setMiddlewareErrorLogger(app: express.Application) {
app.use(expressWinston.errorLogger(getOptionsLogger()));
}
/**
* Cors config
* @param app
*/
function setCorsConfig(app: express.Application) {
app.use(helmet());
app.use(function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE');
res.header('Access-Control-Expose-Headers', 'Content-Length');
res.header('Access-Control-Allow-Headers', req.header('Access-Control-Request-Headers'));
if (req.method === 'OPTIONS') {
return res.status(200).send();
} else {
return next();
}
});
}
export function initServer(app: express.Application) {
const server: http.Server = http.createServer(app);
const port = 3000;
// Load body parser
app.use(bodyParser.json({limit: '5mb'}));
// API logger
setMiddlewareLogger(app);
// cors configuration
setCorsConfig(app);
// Routes
setupRoutes(app);
// API routes error logger
setMiddlewareErrorLogger(app);
server.listen(port, () => {
console.log(`scaffold-api running at port ${port}`);
});
}
12. Índice de nuestra aplicación
Ya está lista nuestra configuración de Express, sin embargo nos falta una pieza en este pequeño rompecabezas: un archivo que nos ayude a iniciar toda nuestra API. Para eso, crearemos un archivo llamado “app.ts” dentro de la carpeta “app”, que contendrá lo siguiente:
import express from "express";
import {initServer} from "./config/server.config";
const app: express.Application = express();
initServer(app)
export default app;
13. Comandos de Package.json
Para poder transpilar nuestro código y ejecutarlo, agregaremos dos configuraciones a los scripts de nuestro package.json.
"dev": "nodemon -e ts --exec \"tsc -p ./tsconfig.json && node ./dist/app.js\"",
"prod": "tsc -p ./tsconfig.json && node ./dist/app.js",
Resultados
Al ejecutar el comando “npm run dev”, podemos ver que se compila nuestro código y se crea una carpeta llamada “dist”. Posteriormente, se ejecuta nuestro servidor con Node.js. La estructura de nuestro proyecto es la siguiente, ignorando “node_modules” y “dist”.
.
├── app
│ ├── app.ts
│ ├── config
│ │ └── server.config.ts
│ ├── controllers
│ │ └── health.controller.ts
│ └── routes
│ ├── abstract.router.ts
│ ├── health.router.ts
│ ├── index.ts
│ └── interfaces
│ └── router.interface.ts
├── package.json
├── package-lock.json
├── README.md
└── tsconfig.json
La salida de nuestra consola es la siguiente:
> scaffold-api-typescript@1.0.0 dev
> nodemon -e ts --exec "tsc -p ./tsconfig.json && node ./dist/app.js"
[nodemon] 2.0.22
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: ts
[nodemon] starting `tsc -p ./tsconfig.json && node ./dist/app.js`
Routes configured for HealthRouter
marketplace-api running at port 3000
Al acceder a “http://localhost:3000/api/v1/health” se muestra un JSON con el siguiente contenido:
{"expressVersion":"4.17.17"}
Conclusión
En este caso, el código requerido para configurar Express fue mucho mayor que el código objetivo de nuestra API. Sin embargo, gracias a la estructura propuesta, es posible separar aún más nuestra lógica de la infraestructura. Esto nos ayuda a escalar, refactorizar y reutilizar código de una manera más simple. En otros posts, mostraré una manera de estandarizar nuestras respuestas y también de utilizar un contenedor de dependencias, teniendo en cuenta la sustitución de Liskov e inversión de dependencias.
Rama de este post: https://github.com/gouh/scaffold-api-typescript/tree/config-express-typescript-health