Creiamo una REST API con Node.js e MongoDB

Creiamo una REST API con Node.js e MongoDB

In questo articolo vedremo come realizzare una applicazione con Node.js che esponga dei servizi REST (una API REST) per l'accesso ad un database NoSQL MongoDB.

Documentiamo la nostra REST API con Swagger
Creaimo un server di mappe geospaziali con Node.js e Mapbox
Gestiamo i processi Node.js con PM2

In questo articolo vedremo come realizzare una applicazione con Node.js che esponga dei servizi REST (una API REST) per l’accesso ad un database NoSQL MongoDB.

REST è l’acronimo di REpresentational State Transfer ed è uno stile architetturale che si applica ai sistemi distribuiti. Tipicamente viene utilizzato per accedere e manipolare dei dati, visti come risorse, disponibili sul Web, utilizzando operazioni stateless. Queste operazioni sono parte integrante del protocollo HTTP e corrispondono alle funzionalità CRUD essenziali (Create, Read, Update e Delete che in italiano sarebbero Crea, Leggi, Aggiorna, Elimina). L’insieme di queste operazioni compone la nostra API (Application Program Interface):

  • POST (creare una risorsa o generalmente fornire dati)
  • GET (recupera un indice di risorse o una singola risorsa)
  • PUT (crea o sostituisce una risorsa)
  • PATCH (aggiorna / modifica una risorsa)
  • DELETE (rimuovi una risorsa)

Per l’accesso al database utilizzeremo una libreria disponibile per Node.js di nome Mongoose, che ci permette di modellare i nostri dati applicativi in modo che sia semplice gestirli con MongoDB. In altre parole, Mongoose è una libreria ODM (object data modeling) per MongoDB; per chi viene dal mondo Java o .Net potrebbe conoscere Hibernate che è molto simile (in realtà è una libreria che fornisce un servizio ORM, Object-Relational Mapping).

Come database, anziché installarne uno ex novo in locale, utilizzeremo una istanza cloud ospitata su MongoDB Atlas (come descritto in questo articolo precedente). Dovremo solo recuperare la connection string dal pannello di controllo come mostrato in figura:

Recuperiamo la connection string per connetterci al database MongoDB Atlas

Questa stringa di connessione la andremo ad inserire in un file .env valorizzando la proprietà MONGODB_URL.

Per modellare ed esporre la nostra API REST, invece utilizzeremo Express, un framework Web di routing e middleware, che ci consente di scrivere facilmente ed in maniera chiara le varie route (le url che scriveremo nel nostro browser) di accesso alla nostra API. 

La nostra applicazione di REST API dipende dunque dai seguenti pacchetti:

  • express
  • mongoose

L’applicazione che realizzeremo ha inoltre anche delle dipendenze di “sviluppo” (sezione devDependencies del file package.json):

  • babel
  • dotenv
  • nodemon

Utilizzeremo babel perchè scriviamo il nostro codice in Javascript ES6 e Node.js non è ancora compatibile con esso (almeno fino alla versione 13, per il quale è compatibile solo in modalità experimental). babel si occupa di “tradurre” (babel è un transpiller, un compilatore da sorgente a sorgente) il codice ES6 in codice ES5 interpretabile da Node.js.

dotenv invece ci consente di accedere a delle variabili contenute in un file .env in modo da avere delle costanti da poter riutilizzare nel codice, una sorta di variabili di ambiente.

nodemon infine ci permette di “ricompilare” e riavviare il server Node.js in automatico quando modifichiamo qualcosa nel nostro codice sorgente.

Creiamo la nostra API REST con Node.js e MongoDB

Creiamo l’entry point della nostra API REST con Node.js e MongoDB che chiameremo app.js:

import express from 'express'
import load from 'dotenv'
import path from 'path'
import DBConnection from './db/DBConnection'
import postRouter from './routers/post'

// configure dotenv to retrieve environment variables
load.config()

console.log(`Connecting to ${process.env.MONGODB_URL} uri`)

DBConnection(process.env.MONGODB_URL)
  .then(() => console.log('Connection ok'))
  .catch(error => console.log('Connection to MongoDB failed'))

// use express as framework
var app = express()

// Middlewares
// to support JSON-encoded bodies
app.use(express.json())
// to support URL-encoded bodies
app.use(express.urlencoded({extended: true})); 

app.use('/posts', postRouter)

const port = process.env.PORT
app.listen(port, () => {
    console.log(`Server running on port ${port}`)
})

Nella prima sezione importiamo i moduli che ci serviranno nel seguito, poi configuriamo dotenv per poter accedere a delle variabili di ambiente definite in un file .env, che riportiamo di seguito:

MONGODB_URL=mongodb+srv://<username>:<password>:@<cluster-address>/<database-name>
PORT=4000

Abbiamo definito una porta (4000) dove risponderà il nostro server Node.js e l’url di accesso al database cloud MongoDB. Per la proprietà MONGODB_URL utilizzero la connection string recuperata da MongoDB Atlas, come detto in precedenza.

Definiamo inoltre una route (un endpoint) della nostra applicazione e vi agganciamo un router che gestirà le varie richieste a quella URI (nel nostro caso /posts):

app.use('/posts', postRouter)

dunque la nostra applicazione, delega a quel router di rispondere ad un URL base del tipo:

http://localhost:4000/posts/

Definiamo la connessione a MongoDB

La connessione al db MongoDB avviene nel file DBConnection.js

import mongoose from 'mongoose'

function DBConnection(uri) {
    let result = mongoose.connect(uri, {
                useNewUrlParser: true,
                useCreateIndex: true,
                useUnifiedTopology: true
                })
                .catch(err => { //if there are any errors...
                    console.error('DB Connection error:', err.stack)
                    throw err
                })
                .then(() => {
                    console.log("Connection to MongoDB successfully!")
                });
    return result
    }
    
export default DBConnection

Con la funzione connect() di mongoose ci colleghiamo al database e gestiamo eventuali eccezioni.

Definiamo poi lo schema del record che vogliamo gestire. Supponiamo che vogliamo gestire dei record che denominiamo Post (ad esempio potrebbero essere articoli di un blog o post su di un social network come Facebook o Twitter) che hanno tre campi:

  • title: di tipo stringa, obbligatorio
  • description: di tipo stringa, obbligatorio
  • date: di tipo Date, che viene valorizzato di default con la data odierna

Dobbiamo descrivere questo modello e in mongoose lo si può fare utilizzando uno Schema, che verrà poi mappato su una Collection (una tabella) di MongoDB:

import mongoose from 'mongoose'

const postSchema = mongoose.Schema({
    title: {
        type: String,
        required: true,
        trim: true
    },
    description: {
        type: String,
        required: true
    },
    date: {
        type: Date,
        default: Date.now
    }
})

const Post = mongoose.model('Post', postSchema)

export default Post

Definiamo le operazioni REST della nostra API

Per quanto riguarda le operazioni REST, queste sono definite nel file Javascript /routers/post.js:

import express from 'express'
import mongoose from 'mongoose'
import Post from '../models/Post'

const router = express.Router()

// Get All Post 
router.get('/', async (req, res) => {
    try {
        let posts = await Post.find()        
        if (!posts) {
            return res.status(401).send({error: 'Post not found'})
        }
        res.status(200).send(posts);
    } catch (error) {
        res.status(400).send(error)
    }
})

// Get a Post by query id
router.get('/findById', async (req, res) => {
    try {
        // View post with id = req.id
        console.log('Id : ' + req.query.id)
        //Check if is a valid MongoDB Id
        let valid = mongoose.Types.ObjectId.isValid(req.query.id);
        if (!valid) {
            return res.status(400).send({error: 'Not a ObjectId id for post'})
        }
        let post = await Post.findById(req.query.id)        
        if (!post) {
            return res.status(401).send({error: 'Post not found'})
        }
        res.status(200).send(post);
    } catch (error) {
        res.status(400).send(error)
    }
})

// Get a Post by param postId
router.get('/:postId', async (req, res) => {
    try {
        // View post with id = req.params.postId
        console.log(`Find by postId : ${req.params.postId}`)
        //Check if is a valid MongoDB Id
        let valid = mongoose.Types.ObjectId.isValid(req.params.postId);
        if (!valid) {
            return res.status(400).send({error: 'Not a valid ObjectId for post'})
        }
        let post = await Post.findById(req.params.postId)        
        if (!post) {
            return res.status(401).send({error: 'Post not found'})
        }
        res.status(200).send(post);
    } catch (error) {
        res.status(400).send(error)
    }
})

// Send a Post
router.post('/', async (req, res) => {
    // Create a new post    
    console.log(req.body)
    try {
        const post = new Post(req.body)
        await post.save()
        res.status(201).send({ post})
    } catch (error) {
        res.status(400).send(error)
    }
})

// Update a Post
router.put('/', async (req, res) => {
    // Create a new post    
    console.log(req.body)
    try {
        const updatedPost = await Post.updateOne({_id: req.body.id}, req.body)
        res.status(201).send(updatedPost)
    } catch (error) {
        res.status(400).send(error)
    }
})

// Delete a Post
router.delete('/', async (req, res) => {
    // Create a new post    
    console.log(req.body)
    try {
        const removedPost = await Post.remove({_id: req.body.id})
        res.status(200).send(removedPost)
    } catch (error) {
        res.status(400).send(error)
    }
})

export default router

Con questo Router Express gestiamo tutte e 4 le principali operazioni CRUD, in particolare per l’operazione di Read (HTTP GET), abbiamo prevesti 3 differenti modi di interrogare il database, chiedendo nella prima funzione tutti i post presenti nella tabella post del database, mentre le successive due funzioni gestiscono la richiesta di un particolare post dato l’id, che può essere passato come query string:

// Get a Post by query id
router.get('/findById', async (req, res) => {
    try {
        // View post with id = req.id
        console.log('Id : ' + req.query.id)
        ...
})

Quindi tramite una url del tipo:

http://localhost:4000/posts/findById?postId=postId

Oppure come URL parameter:

// Get a Post by URL param postId
router.get('/:postId', async (req, res) => {
    try {
        // View post with id = req.params.postId
        console.log(`Find by postId : ${req.params.postId}`)
     ...
})

Quindi tramite una url del tipo:

http://localhost:4000/posts/postId

Ora possiamo lanciare la nostra applicazione con il comando:

npm run server

Oppure se vogliamo modificare “a volo” il codice (utilizzando nodemon) lanciamo il seguente comando

npm run dev-server

Testiamo la nostra API REST con Postman

Una volta avviato il server Node.js possiamo testare la REST API utilizzando il software Postman (che potete scaricare qui).

Una screenshot di Postman

Con Postman possiamo simulare qualunque metodo di chiamata HTTP (GET, POST, DELETE, etc.) selezionandolo dal menù a sinistra della casella di richiesta URL (come mostrato nella precedente figura). Nella casella di richiesta URL (contrassegnata con la label “Enter requested URL”) andiamo invece ad inserire la URL da invocare completa della query string o dai parameters.

Per effettuare invece una chiamata POST dobbiamo passare i parametri tramite body, cliccando sul pulsante Body e scegliendo JSON come tipo di dati:

Inviamo una POST request alla nostra appliczione

Inviamo la richiesta premendo sul pulsante Send. La risposta sarà contenuta nel pannello inferiore:

Body response HTTP
Otteniamo la risposta nel Body response

L’esempio disponibile su GitHub

Il codice Javascript completo dell’applicazione è disponibile su GitHub:

https://github.com/angeloonline/NodeJSRestAPI

Dopo aver clonato il progetto con git clone, dobbiamo installare tutte le dipendenze prima di poterlo avviare, utilizzando il comando:

npm install

tutte le dipendenze verranno installate nella cartella /node_modules

COMMENTS

WORDPRESS: 0