Cover Image

Come creare un semplice NodeJS Rest API con il modulo HTTP

27 Aprile 2020 - Tempo di lettura: 26 minuti

Il più delle volte scriviamo NodeJS Rest API con framework Express o altro. A volte dobbiamo usare il modulo HTTP di base NodeJS per creare un server web.

Che cosa succede se non si ha accesso all'installazione dei moduli NPM o se si desidera creare un endpoint semplice senza molte dipendenze quando si ha poco spazio su disco?

È davvero utile sapere come creare l'API Rest di NodeJS con il modulo core HTTP. In questo post, creerò un'API NodeJS passo dopo passo.

Prerequisiti

Ci sono alcuni prerequisiti per questo progetto. Dobbiamo installare l'ultima versione LTS di Node e qualsiasi editor. Consiglierei VSCode. Stiamo anche creando un container e gira su Docker e Kubernetes.

Semplice Server con modulo HTTP

HTTP è il protocollo a livello di applicazione del modello OSI. NodeJS ha un modulo HTTP con funzionalità sia client che server. Questo modulo viene fornito con il core principale, il che significa che non è necessario installare moduli di Node aggiuntivi per utilizzarlo.

Ecco un semplice server Web HTTP che possiamo creare con questo modulo. Abbiamo importato il modulo HTTP sulla prima riga e creato un server in ascolto sulla porta 3000. La funzione createServer accetta una funzione callback che prende la richiesta e la risposta HTTP e possiamo leggere l'URL e il metodo HTTP dall'oggetto richiesta.

Siamo in grado di inviare i codici di stato e le risposte corrispondenti in base all'URL e al metodo della richiesta. Ad esempio, se il metodo non è GET request stiamo inviando il codice di stato 405.

index.js

const http = require('http');

const port = process.env.PORT || 3000

const server = http.createServer((req, res) => {

    if (req.method !== 'GET') {

        res.end(`{"error": "${http.STATUS_CODES[405]}"}`)

    } else {

        if (req.url === '/') {

            res.end(`<h1>Hello World</h1>`)

        }

        if (req.url === '/hello') {

            res.end(`<h1>Hello</h1>`)

        }

    }

    res.end(`{"error": "${http.STATUS_CODES[404]}"}`)

})

server.listen(port, () => {

    console.log(`Server listening on port ${port}`);

})

Eccone una dimostrazione del funzionamento nei due casi di richiesta (positiva o negativa con errore):

Errore nella richiesta:

Operazioni CRUD

Abbiamo visto come creare un server Http sopra e ora facciamo alcune operazioni CRUD con questo. Tutto è una risorsa e usiamo metodi specifici dal protocollo HTTP per fare queste operazioni. Ci concentriamo anche su quattro metodi HTTP in questo articolo GET, POST, UPDATE e DELETE.

CREATE: creazione di una risorsa se la risorsa non esiste, altrimenti non fa nulla. Questo può essere fatto con il metodo HTTP Post. Questo metodo non è idempotente, il che significa che chiamare questo metodo non produce sempre lo stesso risultato e puoi inviare dati senza esporre i dati nell'URL.

REPLACE: Sostituzione di una risorsa se la risorsa esiste o non esiste. Questo può essere fatto con il metodo HTTP Post. Questo metodo non è idempotente, il che significa che chiamare questo metodo non produce sempre lo stesso risultato e puoi inviare dati senza esporre i dati nell'URL.

UPDATE: aggiornamento di una risorsa se la risorsa esiste. Questo può essere fatto con il metodo HTTP PUT. Questo metodo è idempotente, il che significa che chiamare questo metodo produce sempre lo stesso risultato e puoi inviare dati senza esporre i dati nell'URL.

DELETE: eliminazione di una risorsa se la risorsa esiste. Questo può essere fatto con il metodo DELETE HTTP. Questo metodo è idempotente, il che significa che chiamare questo metodo produce sempre lo stesso risultato e puoi inviare dati senza esporre i dati nell'URL.

Dato che ci stiamo concentrando sul modulo HTTP Nodejs, ho creato un file Users che riflette il comportamento del database. Questa funzione utente ha quattro metodi per: creare l'utente, selezionare gli utenti, aggiornare gli utenti ed eliminare l'utente.

users.js

let users = [

    {id: 1, firstName: "first1", lastName: "last1", email: "abc@gmail.com"},

    {id: 2, firstName: "first2", lastName: "last2", email: "abc@gmail.com"},

    {id: 3, firstName: "first3", lastName: "last3", email: "abc@gmail.com"},

    {id: 4, firstName: "first4", lastName: "last4", email: "abc@gmail.com"}

]

function getUsers() {

    return users;

}

function saveUser(user) {

    const numberOfUsers = users.length

    user['id'] = numberOfUsers + 1

    users.push(user);

}

function deleteUser(id) {

    const numberOfUsers = users.length

    users = users.filter(user => user.id != id);

    return users.length !== numberOfUsers

}

function replaceUser(id, user) {

    const foundUser = users.filter(usr => usr.id == id);

    if (foundUser.length === 0) return false

    users = users.map(usr => {

        if (id == usr.id) {

            usr = {id: usr.id, ...user};

        }

        return usr

    })

    return true

}

const Users = function() {}

Users.prototype.getUsers = getUsers

Users.prototype.saveUser = saveUser

Users.prototype.deleteUser = deleteUser

Users.prototype.replaceUser = replaceUser

module.exports = new Users()

Se guardi il file sopra, tutto ciò che stiamo facendo qui è mantenere un array di utenti e aggiornare l'array ogni volta che viene chiamata la funzione appropriata.

Questi sono i seguenti URL che stiamo cercando di creare con il modulo HTTP di base Nodejs.

Get users: http://localhost:3030/users

Create a user: http://localhost:3030/user con post data

Delete a user: http://localhost:3030/user?id=1 con stringa query

Update a user: http://localhost:3030/user?id=1 con stringa query e post data

Qui il file index.js con la lista completa di tutti quattro gli URL:

const http = require('http')

const qs = require('querystring') 

const url = require('url') 

const Users = require('./users');

const host = process.env.HOST || '0.0.0.0'

const port = process.env.PORT || 3030

const server = http.createServer((req, res) => {

    if (req.method === 'GET') {

        return handleGetReq(req, res)

    } else if (req.method === 'POST') {

        return handlePostReq(req, res)

    } else if (req.method === 'DELETE') {

        return handleDeleteReq(req, res)

    } else if (req.method === 'PUT') {

        return handlePutReq(req, res)

    }

})

function handleGetReq(req, res) {

    const { pathname } = url.parse(req.url)

    if (pathname !== '/users') {

        return handleError(res, 404)

    }

    res.setHeader('Content-Type', 'application/json;charset=utf-8');

    return res.end(JSON.stringify(Users.getUsers()))

}

function handlePostReq(req, res) {

    const size = parseInt(req.headers['content-length'], 10)

    const buffer = Buffer.allocUnsafe(size)

    var pos = 0

    const { pathname } = url.parse(req.url)

    if (pathname !== '/user') {

        return handleError(res, 404)

    }

    req 

    .on('data', (chunk) => { 

      const offset = pos + chunk.length 

      if (offset > size) { 

        reject(413, 'Too Large', res) 

        return 

      } 

      chunk.copy(buffer, pos) 

      pos = offset 

    }) 

    .on('end', () => { 

      if (pos !== size) { 

        reject(400, 'Bad Request', res) 

        return 

      } 

      const data = JSON.parse(buffer.toString())

      Users.saveUser(data)

      console.log('User Posted: ', data) 

      res.setHeader('Content-Type', 'application/json;charset=utf-8');

      res.end('You Posted: ' + JSON.stringify(data))

    })

}

function handleDeleteReq(req, res) {

    const { pathname, query } = url.parse(req.url)

    if (pathname !== '/user') {

        return handleError(res, 404)

    }

    const { id } = qs.parse(query)

    const userDeleted = Users.deleteUser(id);

    res.setHeader('Content-Type', 'application/json;charset=utf-8');

    res.end(`{"userDeleted": ${userDeleted}}`)

}

function handlePutReq(req, res) {

    const { pathname, query } = url.parse(req.url)

    if (pathname !== '/user') {

        return handleError(res, 404)

    }

    const { id } = qs.parse(query)

    const size = parseInt(req.headers['content-length'], 10)

    const buffer = Buffer.allocUnsafe(size)

    var pos = 0

    req 

    .on('data', (chunk) => { 

      const offset = pos + chunk.length 

      if (offset > size) { 

        reject(413, 'Too Large', res) 

        return 

      } 

      chunk.copy(buffer, pos) 

      pos = offset 

    }) 

    .on('end', () => { 

      if (pos !== size) { 

        reject(400, 'Bad Request', res) 

        return 

      } 

      const data = JSON.parse(buffer.toString())

      const userUpdated = Users.replaceUser(id, data);

      res.setHeader('Content-Type', 'application/json;charset=utf-8');

      res.end(`{"userUpdated": ${userUpdated}}`)

    })

}

function handleError (res, code) { 

    res.statusCode = code 

    res.end(`{"error": "${http.STATUS_CODES[code]}"}`) 

} 

server.listen(port, () => {

    console.log(`Server listening on port ${port}`)

});

Ci sono alcune cose che dobbiamo notare qui.

  • Importa il file degli utenti con la funzione richiesta
  • Stiamo verificando il metodo HTTP in ogni richiesta in arrivo e chiamiamo la funzione appropriata con req.method.
  • Abbiamo definito quattro funzioni separate per ogni tipo di metodo HTTP.
  • Abbiamo un altro metodo chiamato handleError per inviare il codice 404 se il percorso non corrisponde per gestire tutte le routes non valide.
  • Stiamo utilizzando i moduli core url e qs per analizzare i dati della stringa di query e trovare i nomi dei percorsi.
  • Stiamo analizzando i dati di post all'interno del metodo req.on.
  • Stiamo verificando la dimensione dei dati di post per prevenire eventuali attacchi DDOS.
  • Stiamo impostando il tipo di contenuto come JSON nelle intestazioni di risposta.
  • Sto usando nodemon per accelerare il processo di sviluppo e puoi installarlo come dipendenza dev solo con il seguente comando e usarlo nella sezione script di package.json.
// installa nodemon
$ npm install nodemon --save-dev

// metti questo dentro lo script dev di package.json
"dev": "nodemon ./index.js localhost 3030",

// fai partire l'applicazione
$ npm run dev

Una volta in esecuzione puoi controllare tutti gli URL con il postman come di seguito:

Build e Docker dell'API

Ora abbiamo visto come eseguire l'API nodejs completa con il modulo HTTP di base da NodeJS. È ora di costruire il progetto. Stiamo usando Webpack per costruire il progetto in modo da non dover mettere tutti i file, le cartelle e i moduli node nel server.

Poiché si tratta di un semplice progetto javascript, non è necessario alcun caricatore aggiuntivo per compilare il nostro codice. Ad esempio, abbiamo bisogno di un caricatore ts per trascrivere il codice typescript in javascript semplice, ma qui non è il caso. Ecco un semplice file di configurazione webpack chiamato webpack.config.js nella root del progetto.

const path = require('path');

module.exports = {
  entry: './index.js',
  mode: 'production',
  target: 'node',
  output: {
    path: path.resolve(__dirname, '.'),
    filename: 'server.bundle.js'
  }

};

Dobbiamo installare un webpack sia a livello globale che locale come dipendenza dev ed eseguire il comando webpack nel terminale. Vedrai server.bundle.js dopo aver eseguito il comando webpack.

$ npm install webapck -g
$ npm install webpack --save-dev

// metti questo nel package.json, sezione scripts
"build": "webpack"

Scriviamo il Dockerfile. È facile tutto ciò che dobbiamo fare è copiare il file server.bundle.js ed eseguire il comando node server.bundle.js

Dockerfile:

FROM node:10-slim

WORKDIR /api

COPY server.bundle.js .

CMD ["node", "server.bundle.js"]

Costruiamo l'immagine, eseguiamola e pubblichiamo l'immagine con i seguenti comandi:

// build the image
docker build -t node-api .

// run the container
docker run -d -p 3030:3030 --name nodeapi node-api

// list containers
docker ps

// docker login and publish
docker login
docker tag node-api your-user/node-http-api

Conclusioni

Questo è davvero un buon caso d'uso quando non si ha accesso all'installazione dei moduli NPM o se si desidera creare un endpoint semplice senza molte dipendenze quando si ha poco spazio su disco.

intopic.it