SD
NestJSGraphQLMongoDBTypeScript

GraphQL Code First avec NestJS : construire une API type-safe avec MongoDB

Découvrez comment créer une API GraphQL robuste et type-safe avec NestJS (Code First) et MongoDB/Mongoose en TypeScript — de la config aux resolvers.

AMAlexis Mouchon9 min de lecture

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 :

L'approche Code First est clairement supérieure dans un projet TypeScript pour plusieurs raisons :

// 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.