Créer un RESTFul API avec AdonisJS 5.0 (incluant l'authentification par token)

Si vous désirez plus de contenu francophone comme celui-ci, cliquer Follow ou suivez-moi sur Twitter

AdonisJS est à Javascript ce que Laravel est à PHP. Adonis est donc un framework backend construit avec l'idée d'apporter une expérience développeur (DX) extraordinaire.

Adonis rend disponible tous les outils que vous avez besoin pour bâtir une application fullstack de A à Z

Aujourd'hui vous allez découvrir comment bâtir un RESTful API avec Adonis. Vous allez même découvrir comment intégré l'authentification par token à votre API.

L'API que vous allez construire est une gestion de tâches. Vous aller créer un API pour créer des tâches, lire, effacer et mettre à jour des tâches

Vous allez également créer un API qui pourra créer un user et faire l'authentification. Ce dernier point peut vous sembler compliqué mais en faite Adonis à un package Auth qui s'occupe de presque tout.

Pré-requis

Avoir des connaissances de base sur les Restful API et sur les frameworks backend MVC

Partie 1 : Création du projet et de l'authentification

Créer un nouveau projet Adonis

$ npm init adonis-ts-app@latest my-project-name

  choisir project structure API

Une fois le project créé, vous pouvez lancer le serveur local :

cd my-project-name
node ace serve -w

Installer et configurer le module de base de données

npm i @adonisjs/lucid
node ace configure @adonisjs/lucid

  choisir SQLite

Ici Adonis va créer une base de donnée SQLite qui sera pré-configuré et accessible de votre application

Installer and configurer le module auth

npm i @adonisjs/auth
node ace configure @adonisjs/auth

    - choisir Lucid
    - Saisir modèle User
    - choisir API Tokens
    - choisir créer la migration
    - choisir utiliser une base de donnée et créer la table pour stocker les Tokens

Le module Auth va vous permettre faire un login avec token

Ajouter le champ username au modèle User
(app/models/user.ts)

@column()
public username: string

Par défaut le champ username n'est pas créer, vous allez donc en créer un.

Ajouter le champ username au fichier de migration User
(database/migrations/xxxxxxxxx_users.ts)

table.string('username', 255).notNullable()
table.string('email', 255)->notNullable().unique()

Lancer la migration (afin de créer la table users)

node ace migration:run

La migration s'occupe de créer et mettre à jour les tables et champs de votre base de données.

Installer le module pour hasher le mot de passe

npm i phc-argon2

Ce module vous servira à crypter le mot de passe de l'utilisateur

Création de la route post pour permettre d'ajouter un User
(start/routes.ts)

Route.post('users', 'AuthController.register')

Création du validator:
(validators/Auth/StoreUserValidator.ts)

node ace make:validator Auth/StoreUser

Les Validator permettent d'annuler un requête si cette requête ne passe pas la validation.

Les Validator retournent également un message d'erreur de validation si la validation de passe pas

import { schema, rules } from @ioc:Adonis/Core/Validator

public schema = schema.create({
    email: schema.string({ trim: true }, [
      rules.email(),
      rules.unique({ table: 'users', column: 'email ' }),
    ]),
    username: schema.string({ trim: true }),
    password: schema.string(),
})

Création du controller

node ace make:controller Auth

Les controller contient toutes les fonctions que les routes vont exécuter

Ajouter une fonction 'register' au controller
(app/controllers/Http/AuthController.ts)

public async register({ request, response } : HttpContextContract) {

  const payload = await request.validate(StoreUserValidator)

  const user = await User.create(payload.user)

  return response.created(user) // 201 CREATED
}

Cette fonction permet de créer un 'user' selon les informations soumis par l'API (email, username et password)

Login

Route.post('users/login', 'AuthController.login')

Création du validator: Validators/Auth/LoginValidator.ts

node ace make:validator Auth/Login
import { schema, rules } from @ioc:Adonis/Core/Validator

public schema = schema.create({
    email: schema.string({}, [rules.email()]),
    password: schema.string()
})

Création de la fonction login dans le controller Auth

public async login({ auth, request, response }: HttpContextContract) {

    const { email, password } = await request.validate(LoginValidator)

    const token = await auth.attempt(email, password)
    const user = auth.user!

    return response.ok({
      "token": token,
      ...user.serialize(),
    })
}

Cette fonction s'occupe de faire l'authentification et retourne un token que le client pourra utiliser pour accéder aux routes protégé.

Get user (start/route.ts)

Route.get('user', 'AuthController.me').middleware(['auth'])
public async me({auth, response} : HttpContextContract) {

  return response.ok({ auth.user })
}

Auth middleware (start/kernel.ts)

Server.middleware.registerNamed({ auth: () => import('App/Middleware/Auth') })

Création du middleware qui permettre de vérifier le token

Création de la route put pour update User

Route.put('users', 'AuthController.update').middleware(['auth'])

Création du validator: Validators/Auth/UpdateUserValidator.ts

node ace make:validator Auth/UpdateUser
import { schema, rules } from @ioc:Adonis/Core/Validator

public schema =  schema.create({
  email: schema.string.optional({ trim: true }, [ 
    rules.email(),
    rules.unique({ table: 'users', column: 'email' }),
  ]),
  username: schema.string.optional({ trim: true }),
  password: schema.string.optional(),
})

Création de la fonction update du controller Auth
(app/Controllers/Http/AuthController.ts)

public async update({ auth, request, response } : HttpContextContract) {

  const payload = await request.validate(UpdateUserValidator)

  const user = await auth.user!.merge(payload).save()

  return response.ok(user) // 200 OK
}

Partie 2 - Création du modèle Task

Création du modèle, de la migration et du controller

node ace make:model Task -cm

L'option -cm va créer le fichier de la migration et le fichier du controller

Ouvrir la migration et ajouter les champs voulus:

public async up () {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id')
      table.integer('user_id').unsigned().references('users.id').onDelete('CASCADE')
      table.string('name').notNullable()
      table.boolean('is_done').defaultTo(false)
      table.timestamp('created_at', { useTz: true })
      table.timestamp('updated_at', { useTz: true })
    })
  }

Ouvrir le modèle et ajouter les columns et la relation belongTo

import { DateTime } from 'luxon'
import { BaseModel, BelongsTo, belongsTo, column, hasMany, HasMany } from '@ioc:Adonis/Lucid/Orm'
import User from './User'

export default class Task extends BaseModel {
  @column({ isPrimary: true })
  public id: number

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime

  @column()
  public name: string

  @column()
  public is_done: boolean

  @belongsTo(() => User)
  public user: BelongsTo<typeof User>
}

Ouvrir le fichier modèle User et ajouter le HasMany

@hasMany(() => Task)
  public tasks: HasMany<typeof Task>

Créer les routes pour le CRUD de Tasks

Route.resource('tasks', 'TaskController').apiOnly()

// Cette ligne de code va créer 5 chemin urls pour le CRUD

// Liste des tâches: GET /tasks (tasks.index)
// Sauvegarder une tâches: POST /tasks (tasks.store)
// Lire une tâche: GET tasks/:id (tasks.show)
// Mise à jour d'une tâche: PUT tasks/:id (tasks.update)
// Effacer une tâche: DELETE tasks/:id (tasks.destroy)

Dans le fichier TasksController créer les 5 actions CRUD

import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Task from 'App/Models/Task'
import StoreTaskValidator from 'App/Validators/Tasks/StoreTaskValidator'
import UpdateTaskValidator from 'App/Validators/Tasks/UpdateTaskValidator'

export default class TasksController {
  public async index ({response}: HttpContextContract) {
    const tasks = await Task.all()
    return response.ok(tasks)
  }

  public async store ({ request, response }: HttpContextContract) {
    const payload = await request.validate(StoreTaskValidator)
    const task = await Task.create(payload)
    return response.created(task)
  }

  public async show ({ response, params }: HttpContextContract) {
    console.log(params)
    const task = await Task.findOrFail(params.id)
    return response.ok(task)
  }

  public async update ({ request, response, params }: HttpContextContract) {
    const task = await Task.findOrFail(params.id)
    const payload = await request.validate(UpdateTaskValidator)
    task.merge(payload).save()
    return response.ok(task)
  }

  public async destroy ({ response, params }: HttpContextContract) {
    const task = await Task.findOrFail(params.id)
    task.delete()
    return response.ok(task)
  }
}

Créer le StoreTaskValidator

node ace make:validator Tasks/StoreTask

public schema = schema.create({
    name: schema.string(),
    is_done: schema.boolean(),
  })

Créer le UpdateTaskValidator

node ace make:validator Tasks/UpdateTask

public schema = schema.create({
    name: schema.string.optinal(),
    is_done: schema.boolean.optional(),
  })

Conclusion

Comme vous avez sans douter constater, la syntaxe de Adonis pour créer un Restful API est très propre. Adonis est un des premiers, sinon le premier framework javascript qui rappel le plaisir et l'efficacité pour les développeurs d'utiliser un framework comme Laravel.

21