Quand une équipe démarre un projet web full-stack en Node.js, le réflexe habituel est de créer deux dépôts : un pour le frontend, un pour l’API.
Sur un projet pour un laboratoire pharma (Biogen) — une application de quiz développée chez Sooyoos — nous avons fait un choix différent : l’API vit dans le même projet que le frontend Next.js, dans la même arborescence, avec une structure de dossiers disciplinée et des conventions claires.
Ce choix a des implications concrètes sur la productivité des développeurs, sur la cohérence du type system, et sur la façon dont s’opère le projet. Il a aussi des limites que nous avons apprises à nos dépens.
Cet article est un retour d’expérience technique. Nous décrivons comment nous avons structuré le projet, pourquoi, ce que ça a changé au quotidien, et dans quels contextes cette approche est pertinente — ou ne l’est pas.
1. Ce que « mixer l’API et le frontend » veut dire concrètement
Avant d’aller plus loin, une clarification s’impose. Il existe plusieurs façons d’organiser un projet full-stack Node.js, et elles ne se valent pas toutes dans tous les contextes.
Les configurations courantes
Configuration 1 : deux dépôts séparés. Un dépôt frontend, un dépôt API.
La communication entre les deux passe par un contrat d’interface (OpenAPI, GraphQL). Les deux équipes déploient indépendamment.
Configuration 2 : monorepository avec packages. Un seul dépôt, mais des packages séparés dans un dossier packages/ ou apps/.
Chaque package a son propre package.json, ses propres dépendances, son propre processus de build. Des outils comme Turborepo ou Nx orchestrent les builds et les caches.
Configuration 3 : monorepository plat. Un seul dépôt, un seul package.json, pas de découpage en packages.
Tout le code partage le même espace de noms TypeScript et les mêmes dépendances. La séparation est purement conventionnelle — des dossiers, des règles d’import.
Sur ce projet, nous avons choisi la configuration 3 — le monorepository plat. Ce n’est pas le choix le plus courant, ni le plus documenté. C’est aussi celui qui exige le plus de discipline, mais qui offre le ratio complexité/gain le plus favorable pour une équipe de deux à quatre développeurs sur un produit unique.
Ce que cela implique
Concrètement, la structure du projet ressemble à ceci :
ricochet-front/
├── package.json # Un seul package.json
├── tsconfig.json # Un seul fichier de config TS
├── src/
│ ├── app/
│ │ └── api/ # API exposée via Next.js Route Handlers
│ │ └── pages/ # Pages Next.js
│ ├── api-core/ # Noyau backend
│ ├── components/ # Composants React
│ ├── etc…
Le dossier src/api-core/ est le pivot de l’architecture. Il contient toute la logique backend — sans dépendre de Next.js.
Il est importé directement par les Route Handlers Next.js (src/app/api/).
2. La structure technique
Next.js Route Handlers comme serveur API
L’API de Ricochet n’est pas un serveur Express séparé. Ce sont des Route Handlers Next.js ( des fichiers route.ts dans le dossier src/app/api/ ) qui exposent des fonctions GET, POST, PATCH, DELETE.
Voici un exemple de handler qui retourne la liste des thématiques disponibles :
// src/app/api/thematics/route.ts
import { NextResponse } from « next/server »;
import { ThematicService } from « @/api-core/services/thematic.service »;
import { ThematicResponseDto } from « @/api-core/dtos/thematic.dto »;
const thematicService = new ThematicService();
export async function GET(): Promise<NextResponse<ThematicResponseDto[]>> {
const thematics = await thematicService.findAll();
return NextResponse.json(thematics);
}
Le handler est fin : il appelle le service et retourne la réponse.
La logique métier se trouve dans le dossier src/api-core/.
Le noyau backend api-core
Le dossier src/api-core est organisé selon un pattern classique en couches :
ricochet-front/
├── src/
│ ├── api-core/
│ │ └── dtos/
│ │ └── entities/
│ │ └── migrations/
│ │ └── repositories/
│ │ └── services/
│ │ └── etc…
Il contient ( entre autres ) :
- Entities — les modèles de base de données.
- Repositories — l’accès à la base de données PostgreSQL via TypeORM.
- Services — la logique métier.
3. Ce que cela change au quotidien
Des types partagés
Dans une architecture deux-dépôts classique, le partage de types entre API et frontend est un problème non trivial.
La solution standard est de générer des types TypeScript depuis un schéma OpenAPI — openapi-typescript, swagger-codegen, etc. — et de versionner ce code généré, ou de l’embarquer dans un package partagé.
C’est fonctionnel mais c’est une charge supplémentaire : maintenir le schéma OpenAPI à jour à chaque changement d’API.
Avec une structure co-localisée, ce problème n’existe pas. Le frontend importe directement les types du dossier src/api-core.
Des commits atomiques
Quand une feature touche à la fois l’API et le frontend ( ce qui est le cas de la majorité des features dans une application web ) avec deux dépôts séparés, il faut coordonner deux branches, deux pull requests, deux déploiements.
Le changement d’API doit précéder ou accompagner le changement frontend, ce qui complique le rollback en cas de problème.
Dans un dépôt unique, un commit contient exactement les changements API et frontend d’une feature. La pull request est une unité cohérente. Le reviewer voit le contrat et son implémentation côte à côte.
Un seul environnement de développement
Un seul npm install. Un seul npm run dev. Pas de synchronisation d’environnements, pas de version de dépendances à aligner entre deux dépôts.
C’est un gain modeste sur le papier, mais sur le terrain — notamment lors des onboardings — c’est non négligeable.
4. Ce que cela change pour l’organisation
La réduction de la surface de coordination
Dans une équipe full-stack de deux à quatre développeurs, le coût de coordination entre deux dépôts ou deux packages distincts est souvent sous-estimé. Chaque changement cross-couche nécessite une synchronisation explicite : quelle version de l’API le frontend attend-il ? Qui merge en premier ? Qui déploie quoi ?
Avec un monorepository unique, ces questions disparaissent. Il n’y a pas de version d’API — il y a un état du système. Le frontend et l’API évoluent ensemble, testés ensemble, déployés ensemble.
Cette simplification est réelle jusqu’à une certaine échelle. Elle devient un frein quand des équipes indépendantes ont besoin de déployer l’API et le frontend à des rythmes différents.
L’onboarding simplifié
Un développeur qui rejoint le projet trouve l’ensemble du système dans un seul dépôt : le schéma de données, les règles métier dans les services, les handlers API, les composants frontend. Il peut suivre le fil d’une feature de bout en bout sans changer de repo, sans chercher quelle version de l’API le frontend consomme.
En pratique, le temps pour être autonome sur une feature se réduit, non pas parce que la codebase est plus simple, mais parce que le contexte est continu.
CI/CD unifiée
Un pipeline. Un linter. Un formateur. Une suite de tests. Sur Ricochet, eslint, prettier et vitest s’appliquent à l’ensemble du projet en une commande.
« lint »: « eslint »,
« test »: « vitest run »,
« check-format »: « prettier –check . »,
« check-types »: « tsc –noEmit »
La vérification de types (tsc –noEmit) couvre l’intégralité du système — front et API — en une passe. Si un changement dans un service de l’API casse un type utilisé dans un composant front, TypeScript le détecte immédiatement.
5. Les limites à ne pas ignorer
L’isolation disciplinaire repose sur des conventions, pas sur des outils
Dans un monorepository plat, rien n’empêche dans l’absolu un composant React d’importer une entité TypeORM directement.
Sur le projet, nous avons maintenu cette discipline manuellement : le frontend n’importe que ce qui est partageable par l’API. L’ESLint peut aider à enforcer ces règles via des règles no-restricted-imports, mais cela demande une configuration explicite.
Sans discipline d’équipe ou règles linter, la structure peut se dégrader progressivement.
Impossible de scaler API et frontend indépendamment
L’API et le frontend tournent dans le même processus Node.js. Si votre API reçoit un pic de charge, vous scalez l’ensemble du process Next.js — y compris le rendu des pages React, qui n’en a pas besoin.
Sur Ricochet, ce n’est pas un problème : les profils de charge du frontend et de l’API sont similaires. Sur une application où l’API est consommée par des clients mobiles à haute fréquence et le frontend Next.js reçoit peu de trafic, le couplage peut être coûteux.
La taille du bundle de production
Le build standalone Next.js embarque l’ensemble des node_modules nécessaires à son exécution. Avec TypeORM dans le même package.json, des dépendances lourdes (drivers PostgreSQL, bibliothèques de décorateurs) se retrouvent dans le bundle, même si elles ne servent qu’à l’exécution serveur des routes API. Le bundler inclut plus que ce que le rendu React nécessite.
Ce n’est pas bloquant, mais c’est une inefficacité à noter sur des environnements contraints.
Conclusion : la bonne architecture est celle qui correspond à votre contexte
Le monorepository plat Next.js + API n’est pas une architecture innovante. C’est une architecture pragmatique, adaptée à un contexte précis : une équipe petite, une stack homogène, un produit unique, et une contrainte de simplicité opérationnelle.
Ce qu’elle apporte est réel : des types partagés, des commits atomiques cross-couche, un onboarding simplifié, une CI unifiée.
Ce qu’elle coûte est également réel : une configuration TypeScript plus complexe, une discipline d’import à maintenir manuellement, et l’impossibilité de scaler les deux couches indépendamment.
Les deux approches — co-location plate vs. séparation — répondent à des contextes différents. Aucune n’est universellement meilleure.
| Repos séparés / packages isolés | Monorepo plat | |
|---|---|---|
| Indépendance de déploiement | Oui | Non |
| Partage de types | Via codegen ou package partagé | Natif |
| Onboarding | Plus fragmenté | Plus simple |
| Scalabilité indépendante | Oui | Non |
| Adapté à plusieurs équipes | Oui | Non |
| Adapté à une équipe unique | Surcoût souvent inutile | Oui |
| Déploiement d’API publique / consommée par tiers | Oui | Possible mais sous-optimal |
Sur le projet Ricochet, le calcul a été favorable. Sur une application avec plusieurs équipes ou une API exposée à des tiers, le choix aurait été différent.
Chez Sooyoos, nous accompagnons des équipes qui font face à ces choix d’architecture au moment de passer à l’échelle ou de moderniser une stack existante.
Vous réfléchissez à l’organisation de votre stack Node.js ? Contactez-nous pour un audit gratuit de 30 minutes. Nous analyserons ensemble la pertinence de votre architecture actuelle et ce qui pourrait être simplifié.
FAQ — Questions fréquentes
Le mélange Next.js + API ne crée-t-il pas un problème de sécurité ?
Non, à condition de respecter une règle : tout le code qui s’exécute côté serveur reste dans un dossier séparé ( api-core ) et dans les Route Handlers de Next.js.
En revanche, si vous importez accidentellement du code serveur dans un composant client, Next.js émettra une erreur de build. Cette protection est intégrée au framework.
Peut-on utiliser cette architecture avec une application mobile comme client additionnel ?
Oui et non — les Route Handlers Next.js exposent une API REST standard, consommable par n’importe quel client HTTP. La colocation ne contraint pas les clients de l’API.
En revanche, si l’API doit être versionnée indépendamment du frontend, ou déployée sur un domaine distinct, une séparation en packages ou en repos devient plus pertinente.
À quelle taille de projet cette architecture devient-elle inadaptée ?
Il n’y a pas de seuil en nombre de fichiers. Le signal d’alerte est organisationnel : quand deux équipes ont besoin de déployer API et frontend à des rythmes différents, ou quand les règles d’import deviennent difficiles à maintenir malgré le linter.
En pratique, pour une équipe de plus de cinq développeurs ou pour une API consommée par plusieurs clients distincts, un découpage en packages séparés — via des outils comme pnpm workspaces ou Turborepo — devient justifiable.