Creación de una API REST con Node y Express

Las API REST son la forma más común de API en la actualidad. En este artículo, crearemos una API REST simple en JavaScript usando Node.js y Express.

Introducción

Las API REST son una forma estándar de la industria para que los servicios web envíen y reciban datos. Utilizan métodos de solicitud HTTP para facilitar el ciclo de solicitud-respuesta y, por lo general, transfieren datos mediante JSON y, más raramente, HTML, XML y otros formatos.

En esta guía, vamos a construir una API REST para administrar libros con Node.js y Express.

En aras de la simplicidad, no usaremos una base de datos, por lo que no necesita experiencia en el uso de una. En su lugar, utilizaremos una matriz de JavaScript simple para almacenar nuestros datos.

¿Qué es una API REST?

REST (Transferencia de estado representacional) es una arquitectura estándar para construir y comunicarse con servicios web. Por lo general, exige que los recursos en la web se representen en un formato de texto (como JSON, HTML o XML) y se puede acceder a ellos o modificarlos mediante un conjunto predeterminado de operaciones. Dado que normalmente construimos API REST para aprovechar HTTP en lugar de otros protocolos, estas operaciones corresponden a métodos HTTP como GET, POST o PUT.

En una colección de datos, como libros, por ejemplo, hay algunas acciones que debemos realizar con frecuencia, que se reducen a: Crear, Leer, Actualizar y Eliminar (también conocido como * Funcionalidad CRUD*).

Una API (interfaz de programación de aplicaciones), como su nombre indica, es una interfaz que define la interacción entre diferentes componentes de software. Las API web definen qué solicitudes se pueden realizar a un componente (por ejemplo, un punto final para obtener una lista de libros), cómo realizarlas (por ejemplo, una solicitud GET) y sus respuestas esperadas.

¿Qué es Express? {#lo que es expreso}

ExpressJS es una de las bibliotecas de servidores HTTP más populares para Node.js, que por defecto no es tan amigable para el desarrollo de API. Con Express, simplificamos el desarrollo de API al abstraer el modelo necesario para configurar un servidor, lo que hace que el desarrollo sea más rápido, más legible y más simple. Puede activar una API prototipo en segundos y un par de líneas de código.

Aunque su uso principal era simplificar las cosas con valores predeterminados sensibles, es altamente personalizable usando funciones llamadas "middleware".

{.icon aria-hidden=“true”}

Nota: Express es muy liviano y está construido sobre de middlleware. Mediante el uso de middleware, puede ampliar y extender su funcionalidad más allá de las funciones ya presentes de forma predeterminada.

Aunque solo vamos a crear una API REST en esta guía, el marco ExpressJS no se limita solo a eso: alojar archivos estáticos, realizar la representación del lado del servidor o incluso usarlo como un servidor proxy no es poco común y el sky's the limit con middleware adicional.

Tipos de solicitudes HTTP

Hay algunos tipos de métodos HTTP que debemos comprender antes de crear una API REST. Estos son los métodos que corresponden a las tareas CRUD:

  • ‘POST’: se usa para enviar datos, generalmente se usa para crear nuevas entidades o editar entidades ya existentes.
  • GET: se usa para solicitar datos del servidor, generalmente se usa para leer datos.
  • PUT: se usa para reemplazar completamente el recurso con el recurso enviado, normalmente se usa para actualizar datos.
  • DELETE: Usado para borrar una entidad del servidor.

{.icon aria-hidden=“true”}

Nota: Tenga en cuenta que puede usar POST o PUT para editar los datos almacenados. Eres libre de elegir si quieres usar PUT, ya que se puede omitir por completo. Sin embargo, sea consistente con los verbos HTTP que usa. Si está utilizando POST tanto para crear como para actualizar, entonces no use el método PUT en absoluto.

Lo que vamos a construir

Vamos a crear una aplicación sencilla para almacenar información sobre libros. En esta app almacenaremos información sobre el ISBN del libro, título, autor, fecha de publicación, editorial y número de páginas.

Naturalmente, la funcionalidad básica de la API será la funcionalidad CRUD. Querremos poder enviarle solicitudes para crear, leer, actualizar y eliminar entidades Book. Por supuesto, una API puede hacer mucho más que esto: proporcionar a los usuarios un punto de acceso para obtener datos estadísticos, resúmenes, llamar a otras API, etc.

Las funcionalidades que no son CRUD dependen de la aplicación y, según la naturaleza de su proyecto, probablemente tendrá otros puntos finales. Sin embargo, prácticamente ningún proyecto puede funcionar sin CRUD.

Para evitar inventar datos de libros, usemos un conjunto de datos de GitHub para obtener algunos detalles de muestra sobre libros.

Configuración del proyecto

Primero, inicialicemos un nuevo proyecto Node.js:

1
$ npm init

Complete la información solicitada según sus requisitos; no tiene que completar todos los campos, pero son una manera fácil de configurar datos identificables para un proyecto. Campos como el nombre son mucho más relevantes para publicar aplicaciones en el Node Package Manager, entre otros campos.

Alternativamente, puede usar la configuración predeterminada agregando el indicador -y a la llamada:

1
$ npm init -y

De cualquier manera, terminará con un proyecto con un archivo package.json. Este es un archivo json que contiene todos los metadatos relevantes en su proyecto y tendrá un aspecto similar a este de forma predeterminada:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "name": "app",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "keywords": [],
  "description": ""
}

La "entrada"/"principal" de su aplicación es el archivo que se debe ejecutar para iniciar el proyecto correctamente, generalmente su secuencia de comandos principal y index.js por defecto.

¡Además, la versión de su aplicación y "scripts" están aquí! Puede proporcionar cualquier cantidad de comandos personalizados en la sección "scripts", con un comando asociado a un alias. Aquí, el alias test es un envoltorio para una declaración echo.

Ejecutarías la prueba de la aplicación a través de:

1
2
3
4
5
6
$ npm test

> [correo electrónico protegido] test /Users/david/Desktop/app
> echo "Error: no test specified" && exit 1

Error: no test specified

A menudo, hay un alias start que enmascara uno o más procesos que deben ejecutarse cuando queremos iniciar una aplicación. En la forma básica, simplemente ejecutamos la página de índice con el nodo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
  "name": "app",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  "author": "",
  "license": "ISC",
  "keywords": [],
  "description": ""
}

Puede poner cualquier cantidad de comandos además de node index.js como el script start y cuando ejecuta npm start, todos se ejecutarán:

1
2
3
4
$ test npm start    

> [correo electrónico protegido] start /Users/david/Desktop/app
> node index.js

{.icon aria-hidden=“true”}

Nota: Dado que solo tenemos un comando en el script de inicio, es funcionalmente equivalente a simplemente llamar a $ node index.js en la línea de comando para iniciar la aplicación.

Ahora que está familiarizado con el proyecto, ¡instalemos Express!

1
$ npm install --save express

Se crea un nuevo archivo en el directorio, junto con un directorio node_modules. El archivo package-lock.json realiza un seguimiento de sus dependencias y contiene sus versiones y nombres:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
  "name": "app",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "accepts": {
      "version": "1.3.7",
      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
      "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
      "requires": {
        "mime-types": "~2.1.24",
        "negotiator": "0.6.2"
      }
    },
    ...

El directorio node_modules en realidad aloja el código de las dependencias y puede crecer bastante rápidamente. Con solo instalar Express, ya tenemos una gran cantidad de módulos instalados y rastreados en el archivo package-lock.json.

Estos módulos son, de hecho, pequeños, por lo que no es un problema de ninguna manera. Al usar el archivo package-lock.json, otro cliente no puede saber qué dependencias descargar y qué versiones usar para poder iniciar correctamente su aplicación.

{.icon aria-hidden=“true”}

Nota: Al realizar el control de versiones con herramientas como Git, se considera una buena práctica no versionar el código fuente de los módulos que utiliza en la aplicación. En términos prácticos, no realice un seguimiento ni envíe node_modules a un repositorio. Otros pueden descargar las dependencias basadas en el package-lock.json crucial que ocurre automáticamente cuando ejecutan la aplicación con npm.

Creación de un punto final simple

Ahora, comencemos a crear una aplicación simple "Hello World". Tendrá un único punto final simple que solo devuelve un mensaje como respuesta a nuestra solicitud para obtener la página de inicio.

Primero, creemos un archivo llamado hello-world.js:

1
$ nano hello-world.js

Luego, importemos el marco Express dentro de él:

1
const express = require('express');

A continuación, vamos a crear una instancia de la aplicación Express:

1
const app = express();

Y configurar nuestro puerto:

1
const port = 3000;

El puerto se usará un poco más tarde, cuando le digamos a la aplicación que escuche las solicitudes. Estas tres líneas son repetitivas, pero lo bueno es que ¡eso es todo lo repetitivo que hay!

Ahora, podemos crear un punto final ‘GET’ simple justo debajo de la plantilla. Cuando un usuario llega al punto final con una solicitud GET, se devolverá el mensaje "Hello World, from express" (y se representará en el navegador o se mostrará en la consola).

Nos gustaría configurarlo para que esté en la página de inicio, por lo que la URL para el punto final es /:

1
2
3
app.get('/', (req, res) => {
    res.send('Hello World, from express');
});

En este punto, comencemos con nuestros clientes:

1
app.listen(port, () => console.log(`Hello world app listening on port ${port}!`))

Ejecutemos la aplicación y visitemos el único punto final que tenemos a través de nuestro navegador:

1
2
$ node hello-world.js
Hello world app listening on port 3000!

node js rest api endpoint result

¡Esta es técnicamente una API funcional! Sin embargo, este punto final realmente no hace mucho. Echemos un vistazo a algunos middleware comunes que serán útiles para el trabajo futuro y crear algunos puntos finales más útiles.

Middleware exprés {#middleware exprés}

Como se mencionó anteriormente, ExpressJS es un servidor HTTP simple y no viene con muchas funciones listas para usar. El middleware actúa casi como extensiones para el servidor Express y brinda funcionalidades adicionales en el "medio" de una solicitud. Muchas extensiones de terceros, como morgan para iniciar sesión, multrar para gestionar la carga de archivos, se utilizan de forma rutinaria.

Por ahora, para comenzar, necesitamos instalar un middleware llamado analizador de cuerpo, que nos ayuda a decodificar el cuerpo de una solicitud HTTP:

1
$ npm install --save body-parser

Analiza el cuerpo de la solicitud y nos permite reaccionar en consecuencia.

Dado que estamos llamando a la API desde diferentes ubicaciones al llegar a los puntos finales en el navegador. También tenemos que instalar el middleware CORS.

Si aún no estás familiarizado con intercambio de recursos de origen cruzado, está bien por ahora. Vamos a instalar el middleware y configurarlo:

1
$ npm install --save cors

Creación de una API REST con Node y Express

Adición de libros

Ahora podemos comenzar a construir nuestra aplicación. Cree un nuevo archivo llamado book-api.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const express = require('express')
const bodyParser = require('body-parser');
const cors = require('cors');

const app = express();
const port = 3000;

// Where we will keep books
let books = [];

app.use(cors());

// Configuring body parser middleware
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

app.post('/book', (req, res) => {
    // We will be coding here
});

app.listen(port, () => console.log(`Hello world app listening on port ${port}!`));

Como puede ver, podemos configurar body-parser importándolo y pasándolo al método app.use, que lo habilita como middleware para la instancia Express app.

Usaremos la matriz books para almacenar nuestra colección de libros, simulando una base de datos.

Hay algunos tipos de tipos de cuerpo de solicitud HTTP. Por ejemplo, application/x-www-form-urlencoded es el tipo de cuerpo predeterminado para los formularios, mientras que application/json es algo que usaríamos al solicitar un recurso usando jQuery o el cliente REST de back-end.

Lo que hará el middleware body-parser es capturar el cuerpo HTTP, decodificar la información y agregarla al req.body. A partir de ahí, podemos recuperar fácilmente la información del formulario, en nuestro caso, la información de un libro.

Dentro del método app.post agreguemos el libro a la matriz de libros:

1
2
3
4
5
6
7
8
9
app.post('/book', (req, res) => {
    const book = req.body;

    // Output the book to the console for debugging
    console.log(book);
    books.push(book);

    res.send('Book is added to the database');
});

Ahora, vamos a crear un formulario HTML simple con los campos: ISBN, título, autor, fecha de publicación, editorial y número de páginas en un nuevo archivo, digamos nuevo-libro.html.

Enviaremos los datos a la API utilizando el atributo acción de este formulario HTML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<div class="container">
    <hr>
    <h1>Create New Book</h1>
    <hr>

    <form action="http://localhost:3000/book" method="POST">
        <div class="form-group">
            <label for="ISBN">ISBN</label>
            <input class="form-control" name="isbn">
        </div>

        <div class="form-group">
            <label for="Title">Title</label>
            <input class="form-control" name="title">
        </div>

        <!--Other fields-->
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
</div>

Aquí, el atributo de nuestra etiqueta <form> corresponde a nuestro punto final y la información que enviamos con el botón enviar es la información que nuestro método analiza y agrega a la matriz. Tenga en cuenta que el parámetro método es POST, al igual que en nuestra API.

Deberías ver algo así cuando abras la página:

node js rest api form

Al hacer clic en "Enviar", nos recibe la declaración console.log(book) de nuestras aplicaciones:

1
2
3
4
5
6
{ isbn: '9781593275846',
  title: 'Eloquent JavaScript, Second Edition',
  author: 'Marijn Haverbeke',
  publish_date: '2014-12-14',
  publisher: 'No Starch Press',
  numOfPages: '472' }

Nota: Tenga en cuenta que, dado que estamos usando una matriz para almacenar datos, los perderemos en nuestro próximo reinicio de la aplicación.

Obtener todos los libros

Ahora vamos a crear un punto final para obtener todos los libros de la API:

1
2
3
app.get('/books', (req, res) => {
    res.json(books);
});

Reinicie el servidor. Si el servidor ya se está ejecutando, presione Ctrl + C para detenerlo primero. Agregue algunos libros y abra http://localhost:3000/books en su navegador. Debería ver una respuesta JSON con todos los libros que ha agregado.

Ahora vamos a crear una página HTML para mostrar estos libros de una manera fácil de usar.

Esta vez, crearemos dos archivos: book-list.html que usaremos como plantilla y un archivo book-list.js que mantendrá la lógica para actualizar/eliminar libros y mostrar ellos en la página:

Comencemos con la plantilla:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<div class="container">
    <hr>
    <h1>List of books</h1>
    <hr>
    <div>
        <div class="row" id="books">
        </div>
    </div>
</div>

<div id="editBookModal" class="modal" tabindex="-1" role="dialog">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">Edit Book</h5>
                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                    <span aria-hidden="true">&times;</span>
                </button>
            </div>

            <div class="modal-body">
                <form id="editForm" method="POST">
                    <div class="form-group">
                        <label for="ISBN">ISBN</label>
                        <input class="form-control" name="isbn" id="isbn">
                    </div>

                    <div class="form-group">
                        <label for="Title">Title</label>
                        <input class="form-control" name="title" id="title">
                    </div>

                    <!--Other fields-->

                    <button type="submit" class="btn btn-primary">Submit</button>
                </form>
            </div>
        </div>
    </div>
</div>
<!--Our JS file-->
<script src="book-list.js"></script>

Con la plantilla terminada, podemos implementar la lógica real para recuperar todos los libros usando JavaScript del lado del navegador y nuestra API REST:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const setEditModal = (isbn) => {
    // We will implement this later
}

const deleteBook = (isbn) => {
    // We will implement this later
}

const loadBooks = () => {
    const xhttp = new XMLHttpRequest();

    xhttp.open("GET", "http://localhost:3000/books", false);
    xhttp.send();

    const books = JSON.parse(xhttp.responseText);

    for (let book of books) {
        const x = `
            <div class="col-4">
                <div class="card">
                    <div class="card-body">
                        <h5 class="card-title">${book.title}</h5>
                        <h6 class="card-subtitle mb-2 text-muted">${book.isbn}</h6>

                        <div>Author: ${book.author}</div>
                        <div>Publisher: ${book.publisher}</div>
                        <div>Number Of Pages: ${book.numOfPages}</div>

                        <hr>

                        <button type="button" class="btn btn-danger">Delete</button>
                        <button types="button" class="btn btn-primary" data-toggle="modal"
                            data-target="#editBookModal" onClick="setEditModal(${book.isbn})">
                            Edit
                        </button>
                    </div>
                </div>
            </div>
        `

        document.getElementById('books').innerHTML = document.getElementById('books').innerHTML + x;
    }
}

loadBooks();

En el script anterior, enviamos una solicitud GET al punto final http://localhost:3000/books para recuperar los libros y luego creamos una tarjeta Bootstrap para cada libro para mostrarlo. Si todo funciona correctamente, debería ver algo como esto en su página:

node js rest api book list

Probablemente haya notado los botones Editar y Crear y sus respectivos métodos. Por ahora, dejémoslos vacíos e implementémoslos sobre la marcha.

Recuperación de un libro por ISBN

Si deseamos mostrar un libro específico al usuario, necesitaremos una forma de recuperarlo de la base de datos (o de la matriz, en nuestro caso). Esto siempre se hace mediante una clave específica para esa entidad. En la mayoría de los casos, cada entidad tiene un id único que nos ayuda a identificarlos.

En nuestro caso, cada libro tiene un ISBN que es único por naturaleza, por lo que no hay necesidad de otro valor de id.

Normalmente, esto se hace analizando el parámetro de URL en busca de un id y buscando el libro con el id correspondiente.

Por ejemplo, si el ISBN es 9781593275846, la URL sería http://localhost:3000/book/9781593275846:

1
2
3
4
app.get('/book/:isbn', (req, res) => {
    // Reading isbn from the URL
    const isbn = req.params.isbn;
});

Aquí, nos presentan las URL parametrizadas. Dado que el ISBN depende del libro, existe potencialmente un número infinito de puntos finales aquí. Al agregar dos puntos (:) a la ruta, podemos definir una variable, asignada a la variable isbn. Entonces, si un usuario visita localhost:3000/book/5, el parámetro isbn será 5.

Puede aceptar más de un parámetro en su URL si tiene sentido en su escenario. Por ejemplo /image/:width/:height, y luego puede obtener esos parámetros usando req.params.width y req.params.height.

Ahora, usando nuestro punto final, podemos recuperar un solo libro:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
app.get('/book/:isbn', (req, res) => {
    // Reading isbn from the URL
    const isbn = req.params.isbn;

    // Searching books for the isbn
    for (let book of books) {
        if (book.isbn === isbn) {
            res.json(book);
            return;
        }
    }

    // Sending 404 when not found something is a good practice
    res.status(404).send('Book not found');
});

Nuevamente reinicie el servidor, agregue un nuevo libro y abra localhost/3000/{your_isbn} y la aplicación devolverá la información del libro.

Eliminación de libros

Al eliminar entidades, generalmente las eliminamos una por una para evitar una gran pérdida accidental de datos. Para eliminar elementos, usamos el método HTTP DELETE y especificamos un libro usando su número ISBN, tal como lo recuperamos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
app.delete('/book/:isbn', (req, res) => {
    // Reading isbn from the URL
    const isbn = req.params.isbn;

    // Remove item from the books array
    books = books.filter(i => {
        if (i.isbn !== isbn) {
            return true;
        }
        return false;
    });

    res.send('Book is deleted');
});

Estamos usando el método app.delete para aceptar solicitudes DELETE. También hemos utilizado el método de filtro de matriz para filtrar el libro con el ISBN relevante para eliminarlo de la matriz.

Ahora implementemos el método deleteBook en el archivo book-list.js:

1
2
3
4
5
6
7
8
9
const deleteBook = (isbn) => {
    const xhttp = new XMLHttpRequest();

    xhttp.open("DELETE", `http://localhost:3000/book/${isbn}`, false);
    xhttp.send();

    // Reloading the page
    location.reload();
}

En este método, enviamos la solicitud de eliminación cuando se presiona el botón y recargamos la página para mostrar los cambios.

Edición de libros {#edición de libros}

Muy similar a eliminar entidades, actualizarlas requiere que tomemos una específica, basada en el ISBN y luego enviemos una llamada HTTP POST o PUT con la nueva información.

Volvamos a nuestro archivo book-api.js:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
app.post('/book/:isbn', (req, res) => {
    // Reading isbn from the URL
    const isbn = req.params.isbn;
    const newBook = req.body;

    // Remove item from the books array
    for (let i = 0; i < books.length; i++) {
        let book = books[i]
        if (book.isbn === isbn) {
            books[i] = newBook;
        }
    }

    res.send('Book is edited');
});

Al enviar una solicitud ‘POST’, dirigida a un ISBN específico, el libro adecuado se actualiza con nueva información.

Como ya hemos creado el modal de edición, podemos usar el método setEditModal para recopilar información sobre el libro cuando se hace clic en el botón "Editar".

También estableceremos el parámetro acción del formulario con la URL del libro en el que se hizo clic para enviar la solicitud:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const setEditModal = (isbn) => {
    // Get information about the book using isbn
    const xhttp = new XMLHttpRequest();

    xhttp.open("GET", `http://localhost:3000/book/${isbn}`, false);
    xhttp.send();

    const book = JSON.parse(xhttp.responseText);

    const {
        title,
        author,
        publisher,
        publish_date,
        numOfPages
    } = book;

    // Filling information about the book in the form inside the modal
    document.getElementById('isbn').value = isbn;
    document.getElementById('title').value = title;
    document.getElementById('author').value = author;
    document.getElementById('publisher').value = publisher;
    document.getElementById('publish_date').value = publish_date;
    document.getElementById('numOfPages').value = numOfPages;

    // Setting up the action url for the book
    document.getElementById('editForm').action = `http://localhost:3000/book/${isbn}`;
}

Para verificar si la función de actualización funciona, edite un libro. El formulario debe ser llenado con la información existente sobre el libro. Cambie algo y haga clic en "Enviar", después de lo cual debería ver el mensaje "El libro está editado".

Conclusión

Así de fácil es construir una API REST usando Node.js y Express. Puede visitar la Documentación exprés oficial para obtener más información sobre el marco si está interesado.

Además, el código que proporcioné es solo para el tutorial, nunca debe usarlo en un entorno de producción. Asegúrese de validar los datos y seguir las mejores prácticas cuando escriba código para producción.

Como de costumbre, el código fuente de este proyecto se puede encontrar en GitHub.