Quel framework backend choisir ?

Voir la vidéo

Node.js est un environnement puissant pour développer des applications backend, mais choisir le bon framework peut être déroutant. Entre Fastify, Hono, Express, Adonis, Nest, Next et autres, il est essentiel de comprendre leurs cas d’usage. Je vous propose d'y voir plus clair à travers cet article.

Pourquoi un framework ?

Node.js inclut un module HTTP natif, mais il reste très bas niveau. Gérer des routes, des paramètres d’URL ou des cookies nécessite d’écrire beaucoup de code manuellement. Si on prend l'exemple d'un simple hello world :

import { createServer } from "node:http";
import { parse } from "node:url";
import { DatabaseSync } from "node:sqlite";

const server = createServer((req, res) => {
  const path = parse(req.url!, true).pathname!;
  const method = req.method;

  if (path.startsWith("/hello/")) {
    const name = path.replace("/hello/", "");
    res.writeHead(200, {
      "Content-Type": "text/html; charset=utf-8",
    });

    res.end(`<html>
      <body style="background: black; color: white;">
      <h1>Bonjour ${name} !</h1>
      </body>
      </html>`);
    return;
  }

}

C'est un exemple simple mais on voit déjà que certaines opérations demandent plus de code que nécessaire et que le système de détection des URLs va être difficile à gérer.

Les micro frameworks

Les micro-frameworks ont pour objectif de simplifier la création d'un serveur HTTP en fournissant quelques outils de base : gestion des routes, capture des paramètres d'URL, lecture des cookies, envoi de réponses JSON, etc. Il existe de nombreux framework dans cette catégorie mais ils proposent plus ou moins la même chose.

import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { DatabaseSync } from "node:sqlite";

const app = new Hono();

const db = new DatabaseSync("database.db");

app.get("/", (c) => {
  return c.text("Hello Hono!");
});

app.get("/hello/:name", (c) => {
  const name = c.req.param("name");
  return c.html(`<html>
      <body style="background: black; color: white;">
      <h1>Bonjour ${name} !</h1>
      </body>
      </html>`);
});

app.get("/posts", (c) => {
  const posts = db.prepare("SELECT * FROM posts").all();
  return c.json(posts);
});

app.post("/posts", async (c) => {
  const body = await c.req.json();
  db.prepare(
    "INSERT INTO posts (title, content, slug, published_at) VALUES (@title, @content, @slug, @published_at)"
  ).run(body);
  c.status(201);
  return c.json(body);
});

serve(
  {
    fetch: app.fetch,
    port: 3000,
  },
  (info) => {
    console.log(`Server is running on http://localhost:${info.port}`);
  }
);

Ils proposent une API simplifiée permettant d'enregistrer des routes auxquels on va associer la logique à éxécuter mais leur rôle s'arrète là. Dès que la logique devient plus complexe (validation de formulaires, manipulation de base de données, authentification...), c'est à vous de vous débrouiller.

Les frameworks "tout en un"

Ces frameworks fournissent tous les éléments nécessaires pour créer une application web complète : routeur, validation, ORM, moteurs de templates, etc. Il existe moins de choix dans cette catégorie mais ces frameworks proposent en général des approches assez différentes.

AdonisJS

AdonisJS s'inspire de l'approche Ruby on Rails / Laravel et se focalise sur la simplicité du code. Par exemple, le code de notre "hello world" ressemblera à ça.

import type { HttpContext } from "@adonisjs/core/http";

export default class HellosController {
  hello(c: HttpContext) {
    return c.view.render("hello", {
      name: c.params.name,
    });
  }
}

Pour la partie base de données, il offre un ORM (Lucid) qui permet de représenter les enregistrements de notre base de données sous forme d'objet JavaScript.

import { DateTime } from "luxon";
import { BaseModel, column } from "@adonisjs/lucid/orm";

export default class Post extends BaseModel {
  @column({ isPrimary: true })
  declare id: number;

  @column()
  declare title: string;

  @column()
  declare slug: string;

  @column()
  declare content: string;

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  declare publishedAt: DateTime;
}

Cet objet eut ensuite être utilisé pour récupérer des données mais aussi pour en créer.

import type { HttpContext } from "@adonisjs/core/http";

import Post from "#models/post";
import { createPostValidator } from "#validators/post";

export default class PostsController {
  index() {
    return Post.all();
  }

  async store(c: HttpContext) {
    // Adonis dispose d'un système de validation des données
    const data = await c.request.validateUsing(createPostValidator);
    return Post.create(data);
  }
}

Comme vous pouvez le voir à travers ces quelques exemple le framework intègre tous les outils dont on pourrait avoir besoin pour créer une application web tout en restant simple d'utilisation.

NestJS

NestJS propose lui une approche plus modulaire pour laquelle vous devrez installer manuellement les modules dont vous avez besoin. L'outil est là pour créer la "colle" qui va rattacher tout ensemble. Notre "Hello world" va ressemble à ça.

import { Controller, Get, Param, Header, Render } from "@nestjs/common";

@Controller()
export class AppController {
  @Get("/hello/:name")
  @Header("Content-Type", "text/html; charset=utf8")
  @Render("hello")
  getHello(@Param("name") name: string) {
    return {
      name: name,
    };
  }
}

Pour la partie base de données, le framework n'impose pas d'ORM particulier et il faut donc choisir la librairie à utiliser.

import { Entity, Column, PrimaryGeneratedColumn } from "typeorm";

@Entity("posts")
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column()
  slug: string;

  @Column("text")
  content: string;

  @Column({ nullable: true, type: "datetime" })
  published_at: Date;
}

Ensuite, cet objet peut être utilisé pour intéragir avec la base de données afin de récupérer ou de modifier des articles.

import {
  Controller,
  Get,
  Post,
  Body,
  Patch,
  Param,
  Delete,
  ValidationPipe,
} from "@nestjs/common";
import { PostService } from "./post.service";
import { CreatePostDto } from "./dto/create-post.dto";
import { UpdatePostDto } from "./dto/update-post.dto";
import { InjectRepository } from "@nestjs/typeorm";
import { Post } from "./entities/post.entity";
import { Repository } from "typeorm";

@Controller("posts")
export class PostController {
  constructor(
    @InjectRepository(Post)
    private postsRepository: Repository<Post>
  ) {}

  @Post()
  create(@Body(new ValidationPipe()) createPostDto: CreatePostDto) {
    const post = new Post();
    post.title = createPostDto.title;
    post.published_at = new Date(createPostDto.published_at);
    post.content = createPostDto.content;
    post.slug = createPostDto.slug;
    return this.postsRepository.save(post);
  }

  @Get()
  findAll() {
    return this.postsRepository.find({
      order: {
        id: "desc",
      },
    });
  }
}

Les frameworks front-end avec capacités backend

Cette dernière catégorie est un peu particulière : il s'agit de frameworks qui se repose sur un framework front-end (React, Vue, Svelte) pour créer un backend. Le point central de ces frameworks est la création de composants qui vont devenir des pages automatiquement en fonction de l'arborescense choisie.

import { db } from "@/lib/db";

type Props = {
  params: Promise<{ name: string }>;
};

// app/hello/[name]/page.tsx
export default async function ({ params }: Props) {
  const name = (await params).name;
  return (
    <div>
      <h1>Bonjour {name}</h1>
    </div>
  );
}

Le code devra ensuite être transpilé pour être converti en logique backend et faire correspondre l'URL à notre composant.

Ils permettent aussi de créer des routes plus générique avec une approche sensiblement identique au micro-frameworks.

import { db } from "@/lib/db";

// app/posts/route.tsx

export async function GET(request: Request) {
  const posts = db.prepare("SELECT * FROM posts ORDER BY id DESC").all();
  return Response.json(posts);
}

export async function POST(request: Request) {
  const post = await request.json();
  db.prepare(
    "INSERT INTO posts (title, content, slug, published_at) VALUES (@title, @content, @slug, @published_at)"
  ).run(post);
  return Response.json(post);
}

Enfin, dans certains de ces frameworks permettent aussi de mélanger le code front-end et le code back-end afin de récupérer des données dynamique directement dans le code d'un composant.

import { db } from "@/lib/db";

export default async function () {
  const posts = db.prepare("SELECT * FROM  posts").all();
  return (
    <div>
      <h1>Les derniers articles</h1>
      <ul>
        {posts.map((p) => (
          <li key={p.id}>{p.title}</li>
        ))}
      </ul>
    </div>
  );
}

Quel framework choisir ?

Chaque catégorie répond à des besoins différents, et il est important de bien comprendre ces différences avant de se lancer dans un projet. Ne choisissez pas un framework trop complexe pour un besoin simple, ni un framework trop limité pour une application complète.

  • Micro-frameworks (Hono, Fastify...) : APIs légères, microservices.
  • Tout-en-un (AdonisJS, NestJS...) : Applications complexes avec base de données, authentification, etc.
  • Frontend avec du backend (Next.js, Nuxt...) : Vous devez ajouter du rendu côté serveur à une application principalement front-end (souvent combinée avec une API utilisant une autre approche).
Publié
Technologies utilisées
Auteur :
Grafikart
Partager