GraphQL et NestJS forment l'un des duos les plus puissants de l'écosystème Node.js pour construire des API modernes. Couplés à MongoDB, ils permettent de livrer une stack backend flexible, fortement typée et facile à faire évoluer. Dans cet article, je vous guide pas à pas pour mettre en place une API GraphQL Code First avec NestJS et Mongoose — la même approche que j'utilise sur mes projets fullstack.
Code First vs Schema First : pourquoi choisir Code First ?
NestJS supporte deux approches pour définir votre schéma GraphQL :
- Schema First : vous écrivez manuellement un fichier
.graphql, puis vous créez les resolvers correspondants. - Code First : vous définissez vos types directement en TypeScript avec des décorateurs, et NestJS génère le schéma automatiquement.
L'approche Code First est clairement supérieure dans un projet TypeScript pour plusieurs raisons :
- Une seule source de vérité (votre code TypeScript)
- Pas de désynchro possible entre le schéma
.graphqlet les resolvers - Autocomplétion et vérification des types à la compilation
- Moins de duplication : une classe TypeScript sert à la fois de type GraphQL et de DTO
// Exemple de type GraphQL défini en Code First
import { ObjectType, Field, ID } from '@nestjs/graphql'
@ObjectType()
export class User {
@Field(() => ID)
id: string
@Field()
email: string
@Field()
username: string
}
Installation et configuration
Commencez par installer les dépendances nécessaires :
npm install @nestjs/graphql @nestjs/apollo @apollo/server graphql
npm install @nestjs/mongoose mongoose
Configurez ensuite le module GraphQL dans app.module.ts :
import { GraphQLModule } from '@nestjs/graphql'
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'
import { MongooseModule } from '@nestjs/mongoose'
import { join } from 'path'
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
sortSchema: true,
playground: process.env.NODE_ENV !== 'production',
}),
MongooseModule.forRoot(process.env.MONGODB_URI),
],
})
export class AppModule {}
L'option autoSchemaFile est la clé du mode Code First : NestJS inspecte vos décorateurs et génère automatiquement le fichier de schéma GraphQL.
Définir un schéma Mongoose + GraphQL sans duplication
L'un des défis de cette stack, c'est que Mongoose a ses propres décorateurs (@Schema, @Prop) et GraphQL a les siens (@ObjectType, @Field). La bonne pratique est de les combiner dans une seule classe :
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { ObjectType, Field, ID } from '@nestjs/graphql'
import { Document } from 'mongoose'
@ObjectType() // Pour GraphQL
@Schema() // Pour Mongoose
export class Article {
@Field(() => ID)
_id: string
@Field()
@Prop({ required: true })
title: string
@Field()
@Prop({ required: true })
content: string
@Field()
@Prop({ default: false })
published: boolean
@Field()
@Prop({ default: Date.now })
createdAt: Date
}
export type ArticleDocument = Article & Document
export const ArticleSchema = SchemaFactory.createForClass(Article)
Cette approche évite de maintenir deux définitions de type séparées — votre modèle Mongoose est votre type GraphQL.
Créer un Resolver avec les opérations CRUD
Le resolver NestJS est l'équivalent GraphQL d'un controller REST. Voici un exemple complet avec Query et Mutation :
import { Resolver, Query, Mutation, Args, ID } from '@nestjs/graphql'
import { ArticlesService } from './articles.service'
import { Article } from './article.schema'
import { CreateArticleInput } from './dto/create-article.input'
@Resolver(() => Article)
export class ArticlesResolver {
constructor(private readonly articlesService: ArticlesService) {}
@Query(() => [Article], { name: 'articles' })
findAll() {
return this.articlesService.findAll()
}
@Query(() => Article, { name: 'article' })
findOne(@Args('id', { type: () => ID }) id: string) {
return this.articlesService.findOne(id)
}
@Mutation(() => Article)
createArticle(@Args('createArticleInput') createArticleInput: CreateArticleInput) {
return this.articlesService.create(createArticleInput)
}
@Mutation(() => Boolean)
removeArticle(@Args('id', { type: () => ID }) id: string) {
return this.articlesService.remove(id)
}
}
Les InputTypes pour les mutations
Pour les mutations, on définit des InputType séparés — jamais réutiliser directement l'ObjectType en entrée :
import { InputType, Field } from '@nestjs/graphql'
import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator'
@InputType()
export class CreateArticleInput {
@Field()
@IsString()
@IsNotEmpty()
title: string
@Field()
@IsString()
@IsNotEmpty()
content: string
@Field({ nullable: true })
@IsBoolean()
@IsOptional()
published?: boolean
}
La combinaison de class-validator avec les InputType GraphQL offre une validation automatique dès l'entrée — ajoutez ValidationPipe globalement dans votre main.ts pour l'activer :
app.useGlobalPipes(new ValidationPipe({ whitelist: true }))
Le service : liaison avec MongoDB via Mongoose
import { Injectable, NotFoundException } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { Model } from 'mongoose'
import { Article, ArticleDocument } from './article.schema'
import { CreateArticleInput } from './dto/create-article.input'
@Injectable()
export class ArticlesService {
constructor(
@InjectModel(Article.name)
private readonly articleModel: Model<ArticleDocument>
) {}
async findAll(): Promise<Article[]> {
return this.articleModel.find().exec()
}
async findOne(id: string): Promise<Article> {
const article = await this.articleModel.findById(id).exec()
if (!article) throw new NotFoundException(`Article #${id} introuvable`)
return article
}
async create(input: CreateArticleInput): Promise<Article> {
const article = new this.articleModel(input)
return article.save()
}
async remove(id: string): Promise<boolean> {
const result = await this.articleModel.findByIdAndDelete(id).exec()
return !!result
}
}
Tester avec Apollo Sandbox
Une fois votre serveur lancé (npm run start:dev), accédez à http://localhost:3000/graphql pour ouvrir Apollo Sandbox. Vous pouvez y tester vos queries directement :
query {
articles {
_id
title
published
createdAt
}
}
mutation {
createArticle(
createArticleInput: {
title: "Mon premier article"
content: "Contenu de test..."
published: true
}
) {
_id
title
}
}
Apollo Sandbox génère automatiquement la documentation depuis votre schéma — un énorme gain de temps pour les équipes et pour l'onboarding.
Bonnes pratiques pour aller plus loin
1. Pagination cursor-based : Pour les listes potentiellement longues, privilégiez la pagination par curseur plutôt que l'offset — plus adaptée à MongoDB.
2. DataLoader pour éviter le problème N+1 : Si vous avez des relations entre entités, utilisez nestjs-dataloader pour batcher les requêtes MongoDB et éviter les chargements en cascade.
3. Guard d'authentification sur les resolvers : Ajoutez @UseGuards(GqlAuthGuard) sur vos resolvers sensibles — j'ai détaillé la mise en place des guards JWT dans un article précédent sur les Guards NestJS.
4. Subscriptions GraphQL : Pour du temps réel (notifications, chat…), NestJS supporte les subscriptions via WebSockets — à explorer si votre use case l'exige.
Conclusion
L'approche Code First avec NestJS, GraphQL et MongoDB offre une expérience développeur exceptionnelle : un seul langage (TypeScript) du schéma à la base de données, une validation automatique, et une documentation générée sans effort. C'est la stack que j'utilise sur mes projets clients quand ils ont besoin d'une API flexible et robuste.
La prochaine étape naturelle est d'ajouter l'authentification JWT (voir l'article sur les guards NestJS) et la gestion des erreurs GraphQL avec des exceptions personnalisées.
Vous avez un projet web qui nécessite une API GraphQL ou une architecture fullstack solide ? N'hésitez pas à me contacter pour en discuter — je serais ravi d'échanger sur vos besoins.