Pourquoi choisir AdonisJS pour vos projets Backend ?

AdonisJS, c’est quoi ?

AdonisJS est un framework backend conçu pour Node.js, qui se distingue par son approche structurée et organisée du développement web.

Inspiré par des frameworks comme Laravel, AdonisJS propose une solution complète et unifiée pour la création d'applications modernes, qu'il s'agisse de sites web, d'API REST ou d'autres types de services backend.

Ce framework se distingue par son approche « tout-en-un », où de nombreuses fonctionnalités indispensables sont déjà intégrées. 

Nous avons expérimenté ce framework dans le cadre d’un projet interne Mobiapps.

Structure d’un projet

Avec AdonisJS, chaque projet commence avec une structure normalisée, rendant l'organisation du code prévisible et logique. Cette structure claire permet au développeur de se concentrer sur la logique métier tout en facilitant la collaboration.

Un projet typique est organisé comme suit :

  • /start : Contient les fichiers de configuration pour le démarrage de l’application, comme le fichier routes.ts qui définit les routes de l’application.
  • /app : C’est le cœur de l’application, où se trouvent les fichiers principaux :
    • Controllers : Les contrôleurs gèrent la logique métier et orchestrent les requêtes/réponses.
    • Models : Les modèles représentent les entités de la base de données via l’ORM .
    • Middleware : Les middlewares traitent les requêtes avant ou après qu’elles atteignent les contrôleurs.
    • Validators : Les validateurs définissent les règles de validation des données entrantes.
  • /config : Contient les fichiers de configuration pour différentes parties du framework comme la base de données, l’authentification, ou les services tiers.
  • /database : Comprend les migrations et d’autres fichiers relatifs à la gestion des données.
  • /node_modules : Contient les dépendances installées via npm.
  • /tests : Utilisé pour organiser et exécuter les tests unitaires et fonctionnels.
  • /bin : Contient des scripts exécutables, tels que ceux pour démarrer ou configurer l’application.
  • server.ts : Point d’entrée de l’application.
  • ace : Interface en ligne de commande d’AdonisJS.

Fonctionnement des éléments clés

Voici ci-après les interaction des différentes composantes d’un projet AdonisJS :

Authentification

  • AdonisJS propose une gestion native des sessions et des jetons JWT. L’authentification peut être configurée pour protéger certaines routes ou actions spécifiques.
  • Les middlewares d’authentification vérifient les droits d’accès des utilisateurs et peuvent rediriger ou renvoyer une erreur si les conditions ne sont pas remplies.

Routing et middlewares

  • Le routing est un processus de gestion des requêtes HTTP afin de les rediriger vers des fonctionnalités spécifiques (création de compte, récupération des utilisateurs …). Dans AdonisJS, l’ensemble du routing se fait dans le start/routes.ts. La création de route est très simple, en ajoutant dans l’objet router nos différents chemins. On peut également les regrouper par group pour les routes ayant la même base. Pour contrôler les accès au page, AdonisJS met en place des middleware.
  • Les middlewares sont une série de fonctions exécutées lors d'une requête HTTP, avant que celle-ci n'atteigne le gestionnaire de routes. Chaque fonction dans la chaîne peut soit terminer la requête, soit la transmettre au middleware suivant.
router
 .group(() => {
   router.get('/me', [UsersController, 'me'])
   router.post('/', [UsersController, 'create'])
   router
     .group(() => {
       router
         .group(() => {
           router.get('/', [UsersController, 'getOne'])
           router.patch('/', [UsersController, 'updateOne'])
           router.delete('/', [UsersController, 'deleteOne'])
         })
         .prefix('/:id')
       router.get('/', [UsersController, 'getAll'])
     })
     .middleware(middleware.role([UserRole.COMPANY_ADMIN]))
 })
 .prefix('/users')
 .middleware(middleware.auth())
 

Pour exemple, les routes décrites via l’extrait de code ci-dessus exposent les URL suivantes sur l’API REST :

Ci-dessous un exemple du middleware qui a pour responsabilité de vérifier si l’utilisateur est connecté et sinon de rediriger l’utilisateur vers la page de login.

export default class AuthMiddleware {
 /**
  * The URL to redirect to, when authentication fails
  */
 redirectTo = '/auth/login'


 async handle(
   ctx: HttpContext,
   next: NextFn,
   options: {
     guards?: (keyof Authenticators)[]
   } = {}
 ) {
   await ctx.auth.authenticateUsing(options.guards, { loginRoute: this.redirectTo })
   return next()
 }
}

On peut vérifier si nos routes sont bien définies avec la commande suivante :

node ace list:routes

Controller

Le rôle d’un contrôleur est de gérer la logique métier liée aux requêtes API, de récupérer les informations et de les donner aux services appropriés. 

Une commande est disponible pour générer rapidement, d’après un modèle, l’ensemble des méthodes basiques du contrôleur : 

node ace make:controller model

Ci-dessous le contrôleur liées à nos utilisateurs :

 export default class UsersController {
 async me({ auth, response }: HttpContext) {
   const user = auth.getUserOrFail()
   return response.ok(user)
 }


 async create({ request, response }: HttpContext) {
   const payload = await request.validateUsing(userCreatingValidator)
   const user = await User.create(payload)
   return response.created(user)
 }


 async getAll({ response }: HttpContext) {
   const users = await User.all()
   return response.ok({
     count: users.length,
     data: users,
   })
 }


 async getOne({ params, response }: HttpContext) {
   const user = await User.find(params.id)
   if (!user) {
     return response.notFound({ message: 'User not found' })
   }
   return response.ok(user)
 }


 async updateOne({ params, request, response }: HttpContext) {
   const user = await User.find(params.id)
   if (!user) {
     return response.notFound({ message: 'User not found' })
   }


   const data = request.only(['status'])
   user.merge(data)
   await user.save()


   return response.ok(user)
 }
  async deleteOne({ auth, params, response }: HttpContext) {
   if (auth.user?.id == params.id) {
     return response.badRequest({ message: 'User authenticated cannot delete itself' })
   }


   const user = await User.find(params.id)
   if (!user) {
     return response.notFound({ message: 'User not found' })
   }
   await user.delete()
   return response.status(200)
 }
}

Modèles de données

Un modèle représente la structure et les propriétés des données échangées et éventuellement les relations avec d’autres modèles (one-to-one, one-to-many, …).

On peut générer des modèles simples avec la commande suivante :

node ace make:model User

Voici pour exemple ci-dessous le modèle de données d’un utilisateur.

export default class User extends compose(BaseModel, AuthFinder) {
 @column({ isPrimary: true })
 public declare id: number

 @column({ columnName: 'first_name' })
 public declare firstName: string

 @column({ columnName: 'last_name' })
 public declare lastName: string

 @column()
 public declare email: string

 @column({ serializeAs: null })
 declare password: string
 
 @column.dateTime({ autoCreate: true })
 public declare createdAt: DateTime

 @column.dateTime({ autoUpdate: true })
 declare updatedAt?: DateTime

 @column()
 declare status: UserStatus

 @column()
 declare role: UserRole

 @column()
 public declare companyId?: number

 @belongsTo(() => Company)
 public declare company: BelongsTo<typeof Company>

 @manyToMany(() => Quiz, {
   pivotTable: 'user_quizzes',
   pivotColumns: ['total'],
   pivotTimestamps: {
     createdAt: 'validation_date',
     updatedAt: false
   }
 })
 public declare quizzes: ManyToMany<typeof Quiz>

 static accessTokens = DbAccessTokensProvider.forModel(User, {
   expiresIn: '30 days',
   prefix: 'oat_',
   table: 'auth_access_tokens',
   type: 'auth_token',
   tokenSecretLength: 40,
 })
}

Validator

AdonisJS propose la librairie VineJS, efficace pour réaliser de la vérification des données en entrée d’une requête API, avant de réaliser une action au sein d’un contrôleur. L’idée est de vérifier que la donnée envoyée par le client est cohérente.

Pour créer un validateur, il est possible d’utiliser la commande suivante :

node ace make:auth register

Voici un exemple de validateur utilisée pour la création d’utilisateur :

export const registerValidator = vine.compile(
 vine.object({
   username: vine.string().minLength(3).maxLength(64), // vérification de la propriété “username” dans les data, elle doit contenir entre 3 et 64 caractères
   email: vine
     .string()
     .email()
     .unique(async (query, field) => {
	// On vérifie ici que la propriété “email” dans les data est bien au format d’une adresse mail et que celle-ci n’est pas déjà référencée dans notre base de données des utilisateurs
       const user = await query.from('users').where('email', field).first()
       return !user
     }),
   password: vine.string().minLength(8).maxLength(32),
 })
)

Base de données et ORM 

  • Lucid est un ORM proposé de base avec AdonisJS. Il offre une API fluide pour interroger ou manipuler les données sans écrire de SQL brut. Cette API est utilisable directement depuis les modèles (all, find, …)
  • Les migrations permettent de versionner la structure de la base de données (ajout ou modification de tables, colonnes, etc.). C’est une succession de scripts qui peuvent être exécutés pour faire évoluer la structure de la base de données.

Voici pour exemple, la création de la table des utilisateurs :

export default class extends BaseSchema {
 protected tableName = 'users'


 async up() {
   this.schema.createTable(this.tableName, (table) => {
     table.increments('id').notNullable()
     table.string('first_name').notNullable()
     table.string('last_name').notNullable()
     table.string('email').notNullable().unique()
     table.string('password').notNullable()


     table.timestamp('created_at').notNullable()
     table.timestamp('updated_at').nullable()


     table.enum('status', Object.values(UserStatus)).defaultTo(UserStatus.ACTIVE)
     table.enum('role', Object.values(UserRole)).defaultTo(UserRole.COMPANY_USER)


     table.integer('company_id').nullable().unsigned().references('id').inTable('companies').onDelete('CASCADE')
   })
 }


 async down() {
   this.schema.dropTable(this.tableName)
 }
}

En résumé, AdonisJS articule ses composants de manière cohérente pour offrir une structure claire et modulaire. Chaque élément joue un rôle précis tout en s’intégrant harmonieusement avec le reste du framework, ce qui simplifie le développement et la maintenance des applications.

Points forts

  • Rapidité de mise en œuvre : AdonisJS facilite le démarrage d’un projet grâce à son architecture pré-établie. Après l’initialisation, les développeurs bénéficient d’un cadre clair et structuré qui accélère la phase de développement.
  • Prise en main intuitive : La structure organisée du framework permet aux développeurs, même débutants, de s’adapter rapidement et de naviguer facilement dans le projet.
  • Documentation exhaustive : La documentation fournie par AdonisJS est complète, claire et bien expliquée, rendant l’apprentissage et l’utilisation accessibles à tous les niveaux de compétence.
  • TypeScript intégré : AdonisJS utilise TypeScript de manière native, apportant des avantages comme la vérification de type, une meilleure lisibilité du code et une réduction des erreurs.
  • Outils intégrés : De nombreuses fonctionnalités essentielles, comme la gestion de la base de données, l’authentification, le routage et la validation des données, sont incluses d’emblée, réduisant ainsi la dépendance à des bibliothèques tierces.
  • Facilité d’extension : Il est simple d’ajouter des dépendances ou des packages externes via la CLI permettant une grande flexibilité.

Limitations

  • Gestion des modifications de modèles : Une difficulté notable réside dans la gestion des modèles de données. Les modifications des modèles n'entraînent pas automatiquement une création des scripts de migration pour les tables correspondantes dans la base de données.
  • Projets volumineux : Dans des projets avec une base de données conséquente, cette limitation peut devenir un frein majeur, rendant les ajustements structurels des données chronophages et susceptibles d'introduire des erreurs si les mises à jour manuelles ne sont pas rigoureusement suivies.
  • Écosystème limité : Bien qu'AdonisJS soit robuste, son écosystème est moins vaste que celui de frameworks plus populaires comme Express ou NestJS. Cela peut limiter le choix de plugins, extensions. La communauté d'utilisateurs est encore en croissance, ce qui peut rendre aussi plus difficile la recherche de solutions spécifiques ou d'exemples concrets en dehors de la documentation officielle.
  • Couplage des outils intégrés : Bien que la philosophie "tout-en-un" soit un atout pour la rapidité de développement, elle peut poser problème si vous souhaitez remplacer un composant intégré (par exemple, l'ORM Lucid) par une alternative externe. Ce remplacement peut nécessiter des ajustements non négligeables.

Ces points ne remettent pas en cause la qualité d'AdonisJS, mais ils peuvent influencer le choix du framework selon les priorités et les contraintes d’un projet.

Conclusion

AdonisJS s'impose comme un backend prometteur et en constante évolution. Il se distingue par sa simplicité d’utilisation et sa richesse fonctionnelle. Il offre une solution complète, rapide à mettre en œuvre, avec une structure standardisée dès l’initialisation du projet, ce qui en fait un choix idéal pour démarrer rapidement un développement.

Cependant, il convient de garder à l'esprit certaines limitations pour des projets de grande envergure, notamment dans la gestion des migrations à la création des modèles de données et tables. Malgré cela, ses atouts en font un choix judicieux pour des applications de petite à moyenne taille.

Chez Mobiapps, notre expérimentation d’AdonisJS a permis de confirmer sa capacité à répondre aux besoins d’un projet backend, tout en favorisant une prise en main rapide, une grande efficacité et une productivité remarquable. Nous continuerons probablement à explorer ses possibilités dans le cadre de futurs projets.

Anthony Aumond - Ingégnieur étude & Développeur senior

Fabien Dhermy - Ingégnieur étude & Développeur confirmé

Eric Rajoelison - Ingégnieur étude & Développeur confirmé