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.
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.
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:
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.
// 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:
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
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.