Présentation
NodeJS est une librairie standard de Javascript qui permet d'écrire des applications sur notre machine, notamment de serveurs
Vous pouvez retrouver la documentation officiel ici
Présentation
MongoDB est une base de données (noSQL) qui enregistre des documents JSON mais ne permet pas de relation !
Vous pouvez retrouver la documentation officiel ici
Premiers pas
Le point d'entrée d'une application Javascript (nodejs, DOM, etc ...) c'est le fichier package.json
Ce fichier contient les informations de notre programme, ainsi que ces dépendances. Il est généré grâce à NPM avec la commande
npm init -y{
"name": "tuto",
"version": "1.0.0",
"description": "Nodejs tutorielle",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "nodemon src/index.js",
"test": "jest --watch --setupFiles dotenv/config",
"preinstall": "node bin/copy-past-env.js"
},
"dependencies": {
"cross-env": "^7.0.3",
...
}
}
Voici à quoi ressemble un package.json
{
"name": "tuto",
"version": "1.0.0",
"description": "Nodejs tutorielle",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "nodemon src/index.js",
"test": "jest --watch --setupFiles dotenv/config",
"preinstall": "node bin/copy-past-env.js"
},
"dependencies": {
"cross-env": "^7.0.3",
...
}
}
Voici les informations de votre projet
(type: module correspond à la nouvelle syntax es6 pour les imports et exports)
{
"name": "tuto",
"version": "1.0.0",
"description": "Nodejs tutorielle",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "nodemon src/index.js",
"test": "jest --watch --setupFiles dotenv/config",
"preinstall": "node bin/copy-past-env.js"
},
"dependencies": {
"cross-env": "^7.0.3",
...
}
}
Tout les scripts de démarrage et de test de notre application.
Pour lancer un scripts il suffit de faire
npm run <nomDuScript>
{
"name": "tuto",
"version": "1.0.0",
"description": "Nodejs tutorielle",
"main": "src/index.js",
"type": "module",
"scripts": {
"start": "nodemon src/index.js",
"test": "jest --watch --setupFiles dotenv/config",
"preinstall": "node bin/copy-past-env.js"
},
"dependencies": {
"cross-env": "^7.0.3",
...
}
}
Ces lignes correspondent au dépendances, ce sont des librairies (package) installé depuis "internet" afin que notre projet fonctionne corréctement
# Instale un paquet, et rajoute une ligne dans "dependencies"
# de notre package.json
$ npm install fastify
# Install tout les paquets spécfifié dans le package.json
$ npm install
# Supprime une dépendance
$ npm remove fastify
# Lance le script "start"
$ npm start
# Lance le script "test"
$ npm testIl est possible à tout moment d'utiliser NPM pour différentes actions :
Fastify c'est une librairie (package) qui permet de construire des API web facilement
Les avantages de fastify sont les suivants :
- rapide
- léger
- il comprend les json schémas
- extensible
vous retrouverez la documentation de fastify en ligne !
Pour installer fastify, rien de plus simple
# Installation de fastify
$ npm install fastifyUtiliser fastify
// Importation du package "fastify"
import fastify from 'fastify'
// Création d'une application. L'option
// logger permet d'afficher dans la console
// les logs du serveur
const app = fastify({ logger: true })
// On démarre l'application sur un port
// et un Host donnée :
app.listen(4545, 'localhost')
Attacher des routes à fastify
// Déclaration d'une GET /hello
app.get('/hello', { options }, async () => {
return 'Hello World'
})
// Déclaration d'une POST /hello
app.post('/hello', { options }, async () => {
return 'Hello World'
})
// Déclaration d'une PATCH /hello
app.patch('/hello', { options }, async () => {
return 'Hello World'
})
// Déclaration d'une PUT /hello
app.put('/hello', { options }, async () => {
return 'Hello World'
})
// Déclaration d'une DELETE /hello
app.delete('/hello', { options }, async () => {
return 'Hello World'
})
Attacher des routes avec paramètres
// Déclaration d'une GET /hello
app.get('/hello/:name', { options }, async (request) => {
// Accéde au paramètre de la route nommé "name"
return `Hello ${request.params.name}`
})Vous pouvez en apprendre plus sur les paramètres de routes juste ici
En fastify il est possible de récupérer les informations de la requête grâce à l'objet request. Il est passé en premier paramètre à toutes nos routes
app.post('/categories', async (request) => {
// Nous pouvons accéder au body de la request
const titre = request.body.titre
// On peut accéder aux headers de la request
const contentType = request.headers.contentType
// Les query strings
const page = request.query.page
})En fastify il est possible de récupérer des paramètres de notre route
app.patch('/categories/:id', async (request) => {
// Nous pouvons accéder au paramètre de la route
const id = request.params.id
})En fastify il est possible de définir des schemas (json schemas)
Ces schemas nous permettent de de définir la forme d'une Requête et d'une Réponse HTTP
Schematiser le "body" d'une requête
app.post('/categories', {
schema: {
body: {
type: 'object',
required: [ 'titre' ],
properties: {
titre: {
type: 'string'
}
}
}
}
}, async () => {
})Schematiser les query string
app.get('/categories', {
schema: {
querystring: {
type: 'object',
properties: {
page: {
type: 'number'
},
limit: {
type: 'number'
}
}
}
}
}, async () => {
})Schematiser les réponses
app.get('/categories', {
schema: {
response: {
200: {
type: 'array',
items: {
type: 'object',
required: [ '_id', 'titre' ],
properties: {
_id: { type: 'string' },
titre: { type: 'string'},
},
},
}
}
}
}, async () => {
})MongoDB est une base de données non relationel
Elle n'utilise pas SQL mais plutôt se base sur le format JSON
L'api de mongo est utilisable nativement en javascript
Les avantages d'une tel de base données sont les suivants:
- Rapide en écriture et lécture
- Simple à prendre en main
- N'échoue jamais (ne sort quasiment jamais d'erreur)
- Natif en javascript
Les désavantages d'une tel de base données sont les suivants:
- Non consistant (nous ne pouvons pas nous assurer de la forme de nos données)
- Non relationnelle (il est impossible d'attacher un document à un autre document ou alors très lent)
- Toute la logique est applicative (aucune logique dans la base de données)
Pour utiliser mongodb il est recommandé d'installer le driver nodejs
# Installation du driver mongodb pour nodejs
$ npm install mongodbPour utiliser mongo il nous faut nous connecter à la base pour cela nous utilisons une URL de connexion
import mongo from 'mongodb'
async function start() {
// Connéction à la base de données,
// de recevons un client
const client = await mongo.MongoClient.connect(
'mongodb://<username>:<password>@<host>:<port>'
)
// Nous pouvons facilement récupérer une base de données
const db = client.db('nomDeLaBDD')
}Afin d'utiliser la base de données dans nos routes fastify, nous pouvons "décorer" notre application :
import fastify from 'fastify'
import mongo from 'mongodb'
async function start() {
// Création d'une application fastify
const app = fastify({ logger: true })
// Connéction à la base de données,
// de recevons un client
const client = await mongo.MongoClient.connect(
'mongodb://<username>:<password>@<host>:<port>'
)
// Nous pouvons facilement récupérer une base de données
const db = client.db('nomDeLaBDD')
// Nous décorons l'application en ajoutant la clef
// "db"
app.decorate('db', db)
// Pour accéder à la base de données il suffit de faire:
// app.db
}En mongo il est possible d'éfféctuer une recherche, vous pouvez utiliser toute une série d'opérateur
// Récupération de tout les documents d'une collection
const documents = await db
.collection('nomDeLaCollection')
.find()
.toArray()
// Nous pouvons filtrer ces documents
const doc2 = await db
.collection('books')
.find({ titre: 'Harry Potter' })
.toArray()
const dc3 = await db
.collection('books')
.find({ titre: { $regex: /^Harry/ } })
.toArray()Il est possible de récupérer un seul document
import mongo from 'mongodb'
// Nous récupérons un document par son ID
const doc = await db.collection('books').findOne({
// Ne pas oublié d'utiliser mongo.ObjectId
// dans nos recherche, sinon l'id ne peut correspondre
_id: mongo.ObjectId('KKDHFKS728238')
})
// Nous récupérons un document par l'un de ces champs
const john = await db.collection('users').findOne({
email: "john.doe@mail.com"
})Il est possible d'insérer une ou plusieurs données
const result = await db.collection('categories').insertOne({
titre: 'Ma catégorie'
})
// Ici result est un objet qui contient de statistique
// sur l'insertion de notre / nos documents :
result.insertedId // Retourne l'id du document tout just inséré
result.insertedCount // Retourne le nombre de document inséré
// Il est possible d'insérer plusieurs résultat à la fois
const result = await db.collection('categories').insertMany([
{ titre: 'Nature' },
{ titre: 'Science' },
])
// Nous pouvons récupérer tous les ids insérés
// dans la base de données sous forme de tableaux
// de string
result.insertedIdsIl est possible de mettre à jour des documents
import mongo from 'mongodb'
const result = await db.collection('categories').updateOne(
// Le premier paramètre correspond aux filtres
// de l'élément que nous voulons mettre ç jour
// Ici l'élément avec l'id données
{ _id: mongo.ObjectId('DJFKDH275623') },
// Le deuxième paramètre correspond au champs que nous
// mettons à jour
{ $set: { titre: 'Mise à jour du titre', description: 'coucou' } },
)Il est possible de supprimer des documents
import mongo from 'mongodb'
const result = await db.collection('categories').deleteOne(
// Le premier paramètre correspond aux filtres
// de l'élément que nous voulons mettre ç jour
// Ici l'élément avec l'id données
{ _id: mongo.ObjectId('DJFKDH275623') },
)Il est possible de personnalisé la réponse HTTP grâce à l'objet Reply. Il est envoyé en second paramètre de toutes nos routes
app.post('/categories', async (request, reply) => {
// Nous pouvons changer le status code
reply.code(201)
// Nous pouvons ajouter des en-tête HTTP
reply.header('Powered-By', 'fastify')
})Les plugins sont un système permettant de séparer notre code en plusieurs modules (fichier js)
// src/routes/categories.js
export default async function categoriesPlugin(app) {
app.get('/categories', async () => {
...
})
}On exporte un plugin fastify :
Le premier paramètre correspond à l'application fastify
// src/routes/categories.js
export default async function categoryPlugin(app, opts) {
app.get('/categories', async () => {
...
})
}On exporte un plugin fastify :
Le second paramètre correspond aux options du plugins
// src/routes/categories.js
export default async categoryPlugin(app, opts) {
app.get('/categories', async () => {
...
})
}Ici nous pouvons utiliser fastify et déclarer des routes, schemas, decorators ...
Attention ! Done doit être appelé à la fin du plugin !
// src/routes/categories.js
export default async function categoryPlugin(app, opts) {
app.get('/categories', async () => {
...
})
}Une fois notre plugin écrit, il est possible dans l'enregistrer :
// src/index.js
import fastify from 'fastify'
import monPlugin from './routes/categories.js'
async function start() {
const app = fastify({ logger: true })
// Nous enregistrons notre plugin
app.register(monPlugin)
}Nous enregistrons un plugin avec les options "opt1, opt2, opt3"
// src/index.js
import fastify from 'fastify'
import monPlugin from './routes/categories.js'
async function start() {
const app = fastify({ logger: true })
// Nous enregistrons notre plugin
app.register(monPlugin, {
opt1: true,
opt2: 'foo',
opt3: 'bar',
})
}En fastify il est possible de déclarer des schemas réutilisable
Pour cela, il faut installer fastify-plugin afin de pouvoir déclarer des schémas globaux à toute notre application
Installation de fastify plugin
$ npm install fastify-pluginNous pouvons écrire un plugin fastify pour nos schémas :
// src/schemas/categories.js
export default (app, opts, done) => {
app.addSchema({
$id: 'category',
type: 'object',
required: [ 'titre' ],
properties: {
titre: { type: 'string' },
},
})
done()
}Une fois écrit, notre plugin doit être enregistré :
// src/index.js
import fastify from 'fastify'
import fp from 'fastify-plugin'
import categorySchema from './schemas/categories.js'
async function start() {
const app = fastify({ logger: true })
app.register(fp(categorySchema))
}Il nous faut pas oublier de "wrapper" notre plugin dans "fp", sinon nos schémas ne seront pas accessible depuis l’extérieur du plugin
// src/index.js
import fastify from 'fastify'
import fp from 'fastify-plugin'
import categorySchema from './schemas/categories.js'
async function start() {
const app = fastify({ logger: true })
app.register(fp(categorySchema))
}Pour réutiliser un schema, il suffit de renseigner la clefs "$ref"
// src/routes/categories
export default (app, opts, done) => {
app.post(
'/categories',
{ schema: { body: { $ref: 'category' } } },
async () => {
..
}
)
done()
}On renseigne le schéma avec l'id "category" sur notre body
// src/routes/categories
export default (app, opts, done) => {
app.post(
'/categories',
{ schema: { body: { $ref: 'category' } } },
async () => {
..
}
)
done()
}Nodemon est un petit outil javascript (package) qui nous permet de redémarrer notre serveur à chaque modification de fichier !
Installation
$ npm install nodemonMise en place
// package.json
{
...
"scripts": {
"start": "nodemon src/index.js",
...
},
...
}
Nous spécifions "nodemon" suivie de notre fichier principal d'application
// package.json
{
...
"scripts": {
"start": "nodemon src/index.js",
...
},
...
}
La configuration nous permet de définir des données variable en fonction des machines !
En fonction de notre poste d'installation ou du serveur et bien certaines données doivent changé (ex: l'url de connexion à la base de données)
La configuration s’effectue via des variables d’environnement afin de respecter les « Twelve Factors App »
Ces variables d’environnement sont déclaré dans un fichier nommé '.env' à la racine de notre projet
Attention !
- Ce fichier ne doit jamais être versionné
- On peut cependant versionné un fichier standard `.env.dist`
Afin d'utiliser ce fichier `.env` en nodejs il nous installer le package dotenv
$ npm install dotenvUne fois le paquet installé, il faut l'importer et lancé la fonction "config"
// src/index.js
import dotenv from 'dotenv'
dotenv.config()
// Nous pouvons maintenant accéder aux
// variables déclarer dans le fichier `.env`
// grâce à
process.env.MA_VARIABLEL'auth. JWT fonctionne grâce à un sytstème de token (le fameux JSON Web Token)
Afin de mettre en place une authentification JWT il nous un plugin fastify : fastify-jwt
$ npm install fastify-jwtAfin d'utiliser le plugin il nous l'enregistrer dans notre application :
// src/index.js
import fastify from 'fastify'
import fastifyJwt from 'fastify-jwt'
async function start() {
const app = fastify({ logger: true })
app.register(fastifyJwt, {
secret: 'clef utilisé pour crypter notre token'
})
}On enregistre le plugin fastify-jwt en lui spécifiant le "secret" utilisé pour crtypter notre token
// src/index.js
import fastify from 'fastify'
import fastifyJwt from 'fastify-jwt'
async function start() {
const app = fastify({ logger: true })
app.register(fastifyJwt, {
secret: 'clef utilisé pour crypter notre token'
})
}Grâce à ce plugin nous pouvons générer un JWT
// src/routes/users.js
export default (app, opts, done) => {
// Création de la route d'authentification
app.post(
'/authenticate',
{ schema: { body: { $ref: 'credential' } } },
async (request, reply) => {
// On récupére et on valide l'utilisateur
const user = // Récupération + Validation
reply.code(201)
return { token: app.jwt.sign(user) }
}
)
done()
}
Ici nous cryptons l'objet user dans un token JWT
// src/routes/users.js
export default (app, opts, done) => {
// Création de la route d'authentification
app.post(
'/authenticate',
{ schema: { body: { $ref: 'credential' } } },
async (request, reply) => {
// On récupére et on valide l'utilisateur
const user = // Récupération + Validation
reply.code(201)
return { token: app.jwt.sign(user) }
}
)
done()
}
Pour authentifier un utilisateur, rien de plus simple :
// src/routes/users.js
export default (app, opts, done) => {
// Création de la route d'authentification
app.get('/categories', async (request, reply) => {
// Calide le token envoyé de le header Authorization
await request.jwtVerify()
})
done()
}
Vérifie que le token est présent ainsi que valide dans le header "Authorization". Si ce n'est pas le cas, le code est intérompus et une erreur http 403 est envoyé !
// src/routes/users.js
export default (app, opts, done) => {
// Création de la route d'authentification
app.get('/categories', async (request, reply) => {
// Calide le token envoyé de le header Authorization
await request.jwtVerify()
})
done()
}
Les cors est une sécurité mise en place par les navigateurs et application mobiles afin de restreindre les requêtes au domaine en cours
Par éxemple depuis le site "nodejs.org" il est impossible de faire une requête sur le site "google.fr" sans mettre en place des en-têtes HTTP CORS
Il est extrémement simple de rajouter les en-têtes HTTP CORS en fastify
Il suffit d'installer et d'utiliser le plugin fastify-cors
$ npm install fastify-corsNous enregistrons le plugin CORS
// src/index.js
import fastify from 'fastify'
import fastifyCors from 'fastify-cors'
async function start() {
const app = fastify({ logger: true })
app.register(fastifyCors, {
origin: true
})
}Le plugin CORS acceptent des options de configuration
// src/index.js
import fastify from 'fastify'
import fastifyCors from 'fastify-cors'
async function start() {
const app = fastify({ logger: true })
app.register(fastifyCors, {
origin: true
})
}