API con Node.js, Express y Prisma

Configuración

Todo proyecto necesita una configuración inicial, la siguiente es una configuración común utilizada en los proyectos de Node.js, pero esta puede variar dependiendo de las necesidades del proyecto.

Inicializar el proyecto

Debemos seleccionar un directorio de trabajo para el proyecto, los proyectos deben ser relativos a este directorio de trabajo y deben ser fácilmente transportados a otra ubicación.

A continuación ejecutamos los comandos para crear el directorio de trabajo del proyecto e ingresar a este:

mkdir checklist-api
cd checklist-api

I> Es posible realizar estas operaciones en la interfaz visual del sistema operativo, pero se proveerán los comandos que son estándares independientes del sistema operativo.

Una vez estamos ubicados en nuestro directorio de trabajo, el primer paso debe ser inicializar el proyecto de Node.js con la utilidad de npm:

npm init -y

W> Todos estos comandos deben ser ejecutados en el directorio de trabajo del proyecto.

El comando anterior genera el archivo package.json que es el manifiesto de nuestro proyecto, allí se pueden editar los valores como la descripción, autor y demás configuraciones.

Vamos a editar la sección de los scripts los comandos para ejecutar la aplicación en modo de desarrollo (dev) o en modo de producción (start):

"scripts": {
  "dev": "node --watch index",
  "start": "node index",
  "test": "echo \"Error: no test specified\" && exit 1"
},

I> La opción --watch está disponible desde la versión 18.11 de Node.js.

Al ejecutar el script en modo de desarrollo permitirá que cada vez que se guarde un cambio en los archivos del proyecto, este reiniciará automáticamente el servidor tomando los nuevos cambios.

Existen otras alternativas a la opción --watch como la librería nodemon o pm2 para configuraciones más robustas.

El script de producción es el estándar utilizado en la industria para ejecutar la aplicación en los servidores en la nube.

A continuación creamos el archivo principal de la aplicación:

touch index.js

Esta es otra convención es comúnmente empleada en los proyectos de Node.js y páginas Web, donde el archivo principal se llama index.

Módulos

Para determinar cuál tipo de módulos se utilizará en el proyecto agregamos una opción más al archivo package.json, esto es muy importante, pues cambiará la sintaxis para importar y exportar los módulos en los archivos del proyecto:

  "license": "ISC",
  "type": "module"
}

Existen dos tipos de módulos Common JS (por defecto) que fueron inicialmente impulsados por Node.js y ES Modules que se han convertido en el estándar en el lenguaje JavaScript. La configuración anterior "type": "module" establece que se utilizaran ES Modules.

Control de versiones

Es posible utilizar también una interfaz gráfica para gestionar el control de versiones con git, a continuación los comandos que son estándares.

Inicializar el repositorio de git:

git init

Crear el archivo .gitignore en la raíz del proyecto:

touch .gitignore

Con el siguiente contenido:

node_modules/
.DS_Store
Thumbs.db

Siempre se debe evitar guardar los directorios generados por las librerías instaladas de Node.js (node_modules), pues estas se reconstruyen en cada entorno y no son parte del código fuente, además de los archivos de presentación preliminar de los directorios generados por Windows y Mac OS.

Herramientas de desarrollo

Vitest

Es importante en todo proyecto realizar pruebas unitarias o de integración, una de las librerías en el ecosistema de JavaScript para ello es Vitest. Esta provee una suite completa para ejecutar los diferentes tipos de pruebas.

Instalaremos la librería Vitest como dependencia de desarrollo:

npm install vitest --save-dev

El comando anterior instala la librería en el directorio de node_modules y la coloca como una dependencia de desarrollo en el archivo package.json en la sección: devDependencies, adicionalmente el comando en el entorno de pruebas será vitest:

  "scripts": {
    "dev": "node --watch index",
    "start": "node index",
    "test": "vitest"
  },
  "keywords": [],
  "author": "Gustavo Morales",
  "license": "ISC",
  "type": "module",
  "devDependencies": {
    "vitest": "^0.34.1"
  }
}

I> La versión de la librería puede variar dependiendo de cuando se instale la dependencia, estas varían con el tiempo.

Más información:

ESLint

Es muy recomendado añadir una herramienta que ayude a comprobar la sintaxis de los archivos para evitar posibles errores y porque no, normalizar la forma en que se escribe el código, para ello utilizaremos una librería llamada ESLint que junto con el editor de texto comprobará la sintaxis de los archivos en tiempo de desarrollo:

npm install eslint --save-dev

ESLint permite seleccionar una guía de estilo ya existente o también poder crear una propia guía, todo depende del común acuerdo del equipo de desarrollo.

El siguiente paso será inicializar la configuración de ESLint:

npx eslint --init

I> La utilidad npx está incluida en la instalación de npm que a su vez es instalada con la versión de Node.js.

Este comando inicializa un asistente de configuración, vamos a contestar las preguntas del asistente de configuración de la siguiente manera:

? How would you like to use ESLint?
  To check syntax only
  To check syntax and find problems
> To check syntax, find problems, and enforce code style
? What type of modules does your project use?
> JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these
? Which framework does your project use?
  React
  Vue.js
> None of these
? Does your project use TypeScript? > (No) / Yes
? Where does your code run? (Press <space> to select, (a) to toggle all, (i) to invert selection
  Browser
> Node
? How would you like to define a style for your project?
> Use a popular style guide
  Answer questions about your style
? Which style guide do you want to follow?
  Airbnb: https://github.com/airbnb/javascript
  Standard: https://github.com/standard/standard
> Google: https://github.com/google/eslint-config-google
  XO: https://github.com/xojs/eslint-config-xo
? What format do you want your config file to be in?
  JavaScript
  YAML
> JSON
Checking peerDependencies of eslint-config-google@latest
The config that you've selected requires the following dependencies:

eslint-config-google@latest eslint@>=5.16.0
? Would you like to install them now? > No / (Yes)
? Which package manager do you want to use?
> npm
  yarn
  pnpm

Una vez finalizado el asistente este generará un archivo en la raíz del proyecto llamado .eslintrc.json, el cual contiene toda la configuración.

Más información:

Visual Studio Code

Esta sección es opcional, de hecho se puede utilizar cualquier editor de texto, pero este editor es muy recomendado, por lo tanto veremos todo lo que ofrece.

El instalador oficial se puede encontrar en su página Web oficial.

Extensiones

Las siguientes extensiones son sugeridas para una mejor experiencia en el desarrollo de aplicaciones con Node.js y JavaScript.

También se puede configurar para que una vez el programador abra el proyecto en Visual Studio Code las muestre como sugerencia para instalar:

mkdir .vscode
touch .vscode/extensions.json

Con el siguiente contenido:

{
  "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}

Configuración

Para añadir la configuración personalizada para el proyecto primero se debe crear el archivo de configuración:

touch .vscode/settings.json

Dentro del archivo de configuración settings.json se coloca la siguiente información:

{
  "editor.formatOnSave": true,
  "editor.renderWhitespace": "all",
  "editor.renderControlCharacters": true,
  "editor.trimAutoWhitespace": true,
  "editor.tabSize": 2,
  "files.insertFinalNewline": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "prettier.singleQuote": true,
  "prettier.trailingComma": "es5"
}

Algunas de estas configuraciones son:

  • editor.formatOnSave: Establecer que siempre se le dé formato al documento a la hora de guardar el archivo.
  • editor.renderControlCharacters: Que muestre todos los espacios en blanco y caracteres especiales
  • editor.trimAutoWhitespace: Remueve todos los espacios en blanco que sobren una vez se guarde el doucmento.
  • files.insertFinalNewline: Dejar una línea en blanco al final de cada archivo (requerido por defecto por ESLint) e indicar cuál será el carácter de fin de línea, esto más que todo para retrocompatibilidad con los sistemas Windows.
  • Establecer el formateador de código de Visual Studio Code como Prettier.
  • Finalmente si deseas añadir una coma al final de algunas estructuras de JavaScript puedes indicarle a Prettier que lo haga, esta es una buena práctica cuando se utiliza git y cada línea nueva no necesita añadir una coma en el elemento anterior.

Formatear código fuente

Necesitamos instalar un plugin para especificar que configuración utilizará Prettier:

npm install eslint-config-prettier --save-dev

Finalmente modificar nuevamente el archivo de .eslint.json de la siguiente manera:

  "extends": ["google", "prettier"],

Depuración con Visual Studio Code

Visual Studio Code ofrece la opción de depurar directamente en el editor donde tenemos el código fuente, lo cual es bastante útil, para llevar a cabo esto se debe crear el archivo de configuración:

touch .vscode/launch.json

Con la siguiente información:

{
  "configurations": [
    {
      "name": "Attach by Process ID",
      "processId": "${command:PickProcess}",
      "request": "attach",
      "skipFiles": ["<node_internals>/**"],
      "type": "node"
    }
  ]
}

Siempre que se haga un cambio en la configuración del archivo package.json se debe reiniciar la aplicación una vez más para tomar los últimos cambios.

Más información:

Crear un Web Server

El objetivo de esta API es administrar un listado de tareas por usuario y que opcionalmente las tareas se pueden organizar en grupos, todo lo anterior con persistencia de datos.

Antes de comenzar el proyecto crearemos un servidor Web sencillo, ya que la aplicación será una REST API Web que se podrá consumir mediante el protocolo HTTP.

A continuación tomamos el ejemplo del Web Server que se encuentra en la documentación de Node.js y lo colocamos en el archivo principal:

// index.js

import http from 'http';

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

En el código de ejemplo anterior, Node.js utiliza el módulo de http para crear un servidor Web que escuchará todas las peticiones en la dirección IP y puerto establecido, como es un ejemplo responderá a cualquier solicitud con la cadena Hello World en texto plano sin formato.

I> La sintaxis del código difiere un poco por la decisión de emplear ES Modules en vez de Common JS.

Para ejecutar la aplicación en modo desarrollo:

npm run dev

Al ejecutar el comando anterior ocupará la Terminal y no permitirá ejecutar nuevos comandos, por lo cual se debe abrir una nueva sesión de Terminal para ejecutar los demás comandos. Es decir, se tendrán siempre dos sesiones de Terminal, una donde se ejecuta el servidor de la aplicación y otra donde se ejecutan los demás comandos.

Para comprobar que todo está funcionando como se espera, accedemos en el navegador Web a la siguiente dirección:

http://localhost:3000/

Configuración y variables de entorno

Cross-env

La aplicación no siempre se ejecutará en ambiente de desarrollo, por lo cual es importante establecer cuál es el ambiente en que se está ejecutando y dependiendo de ello se puede decidir que tipo de configuración se va a utilizar e incluso conectarse a diferentes fuentes de datos.

Para ello emplearemos la variable de entorno process.env.NODE_ENV, que puede ser accedida desde cualquier parte de la aplicación por el objeto global process en Node.js.

Antes de asignar el valor de esta variable de entorno, es muy importante garantizar la compatibilidad entre los diferentes sistemas operativos, porque cada sistema asigna estas variables con diferente sintaxis, se utilizará la librería llamada cross-env, la cual se instala como una dependencia del proyecto y así se asegura que siempre esté disponible en la aplicación:

npm install cross-env

Seguido modificamos la sección de scripts en el archivo package.json para asignarle en valor de la variable en cada script que corresponderán a cada entorno de ejecución:

"scripts": {
  "dev": "cross-env NODE_ENV=development node --watch index",
  "start": "cross-env NODE_ENV=production node index",
  "test": "cross-env NODE_ENV=test vitest"
},

Más información:

Dotenv

En este momento los valores de configuración están almacenados en un archivo JavaScript y sus valores están marcados en el código fuente de la aplicación, lo cual NO es una buena práctica, se estaría exponiendo datos sensibles. La idea es extraer este tipo de información y tenerla en archivos separados por entorno, pues no es la misma fuente de datos de desarrollo que la de producción.

Adicionalmente, se debe distinguir entre la configuración de la aplicación y la del usuario, la primera son valores que necesita la aplicación para funcionar como: número del puerto, la dirección IP y demás, el segundo grupo son más relacionadas con la personalización de la aplicación como: número de objetos por página, personalización de la respuesta al usuario, etc.

La librería recomendada que permite hacer esto muy fácil es dotenv, carga todas las variables especificadas desde un archivo al entorno de Node.js (process.env), si estas no están especificadas en el sistema. Instalamos la librería:

npm install dotenv

A continuación creamos el archivo donde se almacenarán las variables de configuración en la raíz del proyecto llamado:

touch .env

En el archivo creado, agregamos nuestra primera variable:

PORT=3000

Es muy importante NO incluir el archivo .env en el repositorio de git, pues esto es justo lo que se quiere evitar, que las variables y sus valores queden en el código fuente de la aplicación, para ello añadimos una nueva línea en el archivo .gitignore:

node_modules/
.DS_Store
.Thumbs.db
.env

Pero como no se está guardando este archivo en nuestro repositorio, se crea una copia de ejemplo para que se pueda renombrar y utilizar:

cp .env .env.example

A continuación centralizaremos todas las configuraciones en un solo archivo independientemente de su origen, este archivo se llamará config.js en un nuevo directorio llamado app:

mkdir app
touch app/config.js

Con el siguiente contenido:

// app/config.js

import * as dotenv from 'dotenv';

dotenv.config();

export const configuration = {
  server: {
    port: process.env.PORT,
  },
};

La librería dotenv buscará por defecto un archivo llamado .env, y procede a cargar las variables y sus valores en el entorno (process.env). Así, la configuración no estará más en el código fuente de la aplicación y adicionalmente se puede reemplazar el archivo de configuración (.env) en cada entorno donde se publique la aplicación o se puede sobreescribir su valor en el entorno del sistema operativo directamente.

Se modifica el Servidor Web para que tome los valores siempre desde del archivo de configuración:

// index.js

import http from 'http';

import { configuration } from './app/config.js';

const { port } = configuration.server;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});

server.listen(port, () => {
  console.log(`Server running at port: ${port}`);
});

I> Para esta aplicación el hostname no es obligatorio, por lo cual lo se omite en el archivo editado.

Más información: