Construire une API GraphQL typesafe avec Next.js App Router, Yoga et Pothos

GraphQL avec une inférence TypeScript complète — sans génération de code. Pothos est un constructeur de schémas qui vous offre l'autocomplétion et la sécurité des types sur toute votre API GraphQL, tandis que GraphQL Yoga fournit un serveur léger et conforme aux spécifications. Dans ce tutoriel, vous combinerez les deux avec Next.js 15 App Router pour construire une API de gestion de favoris prête pour la production.
Ce que vous apprendrez
À la fin de ce tutoriel, vous saurez :
- Configurer GraphQL Yoga comme route handler dans Next.js 15 App Router
- Définir un schéma GraphQL code-first avec Pothos — sans fichiers SDL, sans codegen
- Construire des requêtes et mutations avec une autocomplétion TypeScript complète
- Ajouter un middleware d'authentification via les fonctions de contexte
- Implémenter la validation des entrées avec Zod via les plugins Pothos
- Connecter un client React avec urql pour la récupération des données
- Gérer les erreurs et états de chargement à la manière GraphQL
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (
node --version) - Une expérience en TypeScript (types, génériques, interfaces)
- Une familiarité avec Next.js App Router (route handlers, Server Components)
- Des bases en GraphQL — comprendre les requêtes, mutations et schémas est utile mais pas obligatoire
- Un éditeur de code — VS Code ou Cursor recommandé
Pourquoi GraphQL en 2026 ?
REST fonctionne bien pour le CRUD simple, mais à mesure que votre frontend grandit, vous rencontrez des problèmes familiers : sur-récupération de données inutiles, sous-récupération nécessitant des requêtes supplémentaires, difficultés de versionnement, et aucune manière standard de décrire votre API.
GraphQL résout ces problèmes en permettant au client de demander exactement ce dont il a besoin en une seule requête. Mais les configurations GraphQL traditionnelles nécessitent la maintenance de fichiers SDL séparés et l'exécution de générateurs de code pour synchroniser les types TypeScript.
Pothos change la donne. Vous définissez votre schéma en TypeScript, et il infère automatiquement tous les types. Combiné avec GraphQL Yoga (un serveur léger et conforme aux spécifications) et Next.js App Router, vous obtenez une API GraphQL typesafe de la base de données à l'interface utilisateur — sans aucune génération de code.
Ce que vous allez construire
Un gestionnaire de favoris avec :
- Une API GraphQL exposée via les route handlers Next.js
- Des requêtes pour lister et rechercher les favoris
- Des mutations pour créer, modifier et supprimer des favoris
- Une authentification utilisateur via le contexte
- Un frontend React utilisant urql pour consommer l'API
Voici la structure finale du projet :
bookmark-app/
├── app/
│ ├── api/
│ │ └── graphql/
│ │ └── route.ts # Handler GraphQL Yoga
│ ├── bookmarks/
│ │ └── page.tsx # Interface des favoris
│ └── layout.tsx
├── graphql/
│ ├── builder.ts # Constructeur de schéma Pothos
│ ├── schema.ts # Schéma racine
│ ├── types/
│ │ ├── bookmark.ts # Type Bookmark + résolveurs
│ │ └── user.ts # Type User
│ └── context.ts # Contexte de la requête
├── lib/
│ ├── db.ts # Base de données en mémoire
│ └── urql.ts # Configuration du client urql
└── package.json
Étape 1 : Créer le projet Next.js
Initialisez une nouvelle application Next.js 15 avec TypeScript :
npx create-next-app@latest bookmark-app --typescript --tailwind --app --src-dir=false
cd bookmark-appSélectionnez les options par défaut. Puis installez les dépendances GraphQL :
npm install graphql graphql-yoga @pothos/core @pothos/plugin-zod zod
npm install urql @urql/next graphql-tagCe que fait chaque package :
| Package | Rôle |
|---|---|
graphql | L'implémentation de référence GraphQL |
graphql-yoga | Serveur GraphQL léger et conforme aux spécifications |
@pothos/core | Constructeur de schémas code-first avec inférence de types |
@pothos/plugin-zod | Intégration de validation Zod pour Pothos |
zod | Validation de schémas à l'exécution |
urql | Client GraphQL léger pour React |
@urql/next | Bindings urql spécifiques à Next.js |
graphql-tag | Template literals pour les requêtes GraphQL |
Étape 2 : Configurer la base de données en mémoire
Pour ce tutoriel, vous utiliserez un stockage en mémoire. En production, remplacez-le par Prisma, Drizzle ou toute couche de base de données.
Créez lib/db.ts :
export interface User {
id: string;
name: string;
email: string;
}
export interface Bookmark {
id: string;
url: string;
title: string;
description: string | null;
tags: string[];
userId: string;
createdAt: Date;
updatedAt: Date;
}
// Données initiales
const users: User[] = [
{ id: "1", name: "Alice", email: "alice@example.com" },
{ id: "2", name: "Bob", email: "bob@example.com" },
];
const bookmarks: Bookmark[] = [
{
id: "1",
url: "https://graphql.org",
title: "GraphQL Official Site",
description: "The official GraphQL documentation and specification",
tags: ["graphql", "api", "documentation"],
userId: "1",
createdAt: new Date("2026-01-15"),
updatedAt: new Date("2026-01-15"),
},
{
id: "2",
url: "https://nextjs.org",
title: "Next.js by Vercel",
description: "The React framework for the web",
tags: ["nextjs", "react", "framework"],
userId: "1",
createdAt: new Date("2026-02-01"),
updatedAt: new Date("2026-02-01"),
},
{
id: "3",
url: "https://pothos-graphql.dev",
title: "Pothos GraphQL",
description: "Code-first GraphQL schema builder for TypeScript",
tags: ["graphql", "typescript", "schema"],
userId: "2",
createdAt: new Date("2026-02-20"),
updatedAt: new Date("2026-02-20"),
},
];
let nextId = 4;
export const db = {
users: {
findMany: () => users,
findById: (id: string) => users.find((u) => u.id === id) ?? null,
findByEmail: (email: string) => users.find((u) => u.email === email) ?? null,
},
bookmarks: {
findMany: (filters?: { userId?: string; tag?: string; search?: string }) => {
let result = [...bookmarks];
if (filters?.userId) {
result = result.filter((b) => b.userId === filters.userId);
}
if (filters?.tag) {
result = result.filter((b) => b.tags.includes(filters.tag!));
}
if (filters?.search) {
const q = filters.search.toLowerCase();
result = result.filter(
(b) =>
b.title.toLowerCase().includes(q) ||
b.url.toLowerCase().includes(q) ||
b.description?.toLowerCase().includes(q)
);
}
return result;
},
findById: (id: string) => bookmarks.find((b) => b.id === id) ?? null,
create: (data: Omit<Bookmark, "id" | "createdAt" | "updatedAt">) => {
const bookmark: Bookmark = {
...data,
id: String(nextId++),
createdAt: new Date(),
updatedAt: new Date(),
};
bookmarks.push(bookmark);
return bookmark;
},
update: (id: string, data: Partial<Omit<Bookmark, "id" | "createdAt">>) => {
const index = bookmarks.findIndex((b) => b.id === id);
if (index === -1) return null;
bookmarks[index] = { ...bookmarks[index], ...data, updatedAt: new Date() };
return bookmarks[index];
},
delete: (id: string) => {
const index = bookmarks.findIndex((b) => b.id === id);
if (index === -1) return false;
bookmarks.splice(index, 1);
return true;
},
},
};Étape 3 : Définir le contexte de la requête
Le contexte GraphQL est l'endroit où vous placez les données par requête comme l'utilisateur authentifié et les connexions à la base de données.
Créez graphql/context.ts :
import { db, type User } from "@/lib/db";
export interface GraphQLContext {
currentUser: User | null;
db: typeof db;
}
export function createContext(request: Request): GraphQLContext {
// En production, extrayez et vérifiez un JWT ou un token de session ici
const userId = request.headers.get("x-user-id");
const currentUser = userId ? db.users.findById(userId) : null;
return {
currentUser,
db,
};
}Dans ce tutoriel, nous simulons l'authentification en lisant un en-tête x-user-id. En production, vous vérifieriez un JWT ou un cookie de session.
Étape 4 : Créer le constructeur de schéma Pothos
Le constructeur de schéma est le coeur de Pothos. C'est ici que vous configurez les plugins, définissez le type de contexte et créez l'instance du constructeur.
Créez graphql/builder.ts :
import SchemaBuilder from "@pothos/core";
import ZodPlugin from "@pothos/plugin-zod";
import type { GraphQLContext } from "./context";
export const builder = new SchemaBuilder<{
Context: GraphQLContext;
Scalars: {
DateTime: {
Input: Date;
Output: Date;
};
};
}>({
plugins: [ZodPlugin],
});
// Enregistrer un scalaire DateTime personnalisé
builder.scalarType("DateTime", {
serialize: (value) => value.toISOString(),
parseValue: (value) => {
if (typeof value !== "string") {
throw new Error("DateTime must be a string");
}
return new Date(value);
},
});
// Initialiser les types Query et Mutation
builder.queryType({});
builder.mutationType({});Remarquez comment vous passez GraphQLContext comme générique. Cela signifie que chaque résolveur aura accès à currentUser et db avec une inférence de types complète — aucun cast nécessaire.
Étape 5 : Définir le type User
Créez graphql/types/user.ts :
import { builder } from "../builder";
builder.objectType("User", {
description: "A registered user",
fields: (t) => ({
id: t.exposeString("id"),
name: t.exposeString("name"),
email: t.exposeString("email"),
bookmarks: t.field({
type: ["Bookmark"],
resolve: (user, _args, ctx) => {
return ctx.db.bookmarks.findMany({ userId: user.id });
},
}),
}),
});
// Requête : obtenir l'utilisateur actuel
builder.queryField("me", (t) =>
t.field({
type: "User",
nullable: true,
resolve: (_root, _args, ctx) => ctx.currentUser,
})
);La méthode exposeString est un raccourci Pothos qui mappe un champ directement sur une propriété de l'objet. Pour les champs calculés comme bookmarks, vous écrivez un résolveur complet.
Étape 6 : Définir le type Bookmark avec les requêtes
C'est le type principal. Créez graphql/types/bookmark.ts :
import { builder } from "../builder";
import { z } from "zod";
// Définir le type objet Bookmark
const BookmarkType = builder.objectType("Bookmark", {
description: "A saved bookmark with URL and metadata",
fields: (t) => ({
id: t.exposeString("id"),
url: t.exposeString("url"),
title: t.exposeString("title"),
description: t.exposeString("description", { nullable: true }),
tags: t.exposeStringList("tags"),
createdAt: t.expose("createdAt", { type: "DateTime" }),
updatedAt: t.expose("updatedAt", { type: "DateTime" }),
user: t.field({
type: "User",
nullable: true,
resolve: (bookmark, _args, ctx) => {
return ctx.db.users.findById(bookmark.userId);
},
}),
}),
});
// Requête : lister tous les favoris avec des filtres optionnels
builder.queryField("bookmarks", (t) =>
t.field({
type: [BookmarkType],
args: {
tag: t.arg.string({ required: false }),
search: t.arg.string({ required: false }),
},
resolve: (_root, args, ctx) => {
return ctx.db.bookmarks.findMany({
tag: args.tag ?? undefined,
search: args.search ?? undefined,
});
},
})
);
// Requête : obtenir un favori par ID
builder.queryField("bookmark", (t) =>
t.field({
type: BookmarkType,
nullable: true,
args: {
id: t.arg.string({ required: true }),
},
resolve: (_root, args, ctx) => {
return ctx.db.bookmarks.findById(args.id);
},
})
);Étape 7 : Ajouter les mutations avec validation Zod
Ajoutez les mutations de création, modification et suppression dans le même fichier graphql/types/bookmark.ts :
// Schémas Zod pour la validation des entrées
const CreateBookmarkInput = z.object({
url: z.string().url("Must be a valid URL"),
title: z.string().min(1, "Title is required").max(200),
description: z.string().max(1000).nullable().optional(),
tags: z.array(z.string().max(50)).max(10).default([]),
});
const UpdateBookmarkInput = z.object({
url: z.string().url("Must be a valid URL").optional(),
title: z.string().min(1).max(200).optional(),
description: z.string().max(1000).nullable().optional(),
tags: z.array(z.string().max(50)).max(10).optional(),
});
// Mutation : créer un favori
builder.mutationField("createBookmark", (t) =>
t.field({
type: BookmarkType,
args: {
input: t.arg({
type: builder.inputType("CreateBookmarkInput", {
fields: (t) => ({
url: t.string({ required: true }),
title: t.string({ required: true }),
description: t.string({ required: false }),
tags: t.stringList({ required: false, defaultValue: [] }),
}),
}),
required: true,
}),
},
resolve: (_root, args, ctx) => {
if (!ctx.currentUser) {
throw new Error("You must be logged in to create a bookmark");
}
const validated = CreateBookmarkInput.parse({
url: args.input.url,
title: args.input.title,
description: args.input.description,
tags: args.input.tags,
});
return ctx.db.bookmarks.create({
...validated,
description: validated.description ?? null,
tags: validated.tags,
userId: ctx.currentUser.id,
});
},
})
);
// Mutation : modifier un favori
builder.mutationField("updateBookmark", (t) =>
t.field({
type: BookmarkType,
nullable: true,
args: {
id: t.arg.string({ required: true }),
input: t.arg({
type: builder.inputType("UpdateBookmarkInput", {
fields: (t) => ({
url: t.string({ required: false }),
title: t.string({ required: false }),
description: t.string({ required: false }),
tags: t.stringList({ required: false }),
}),
}),
required: true,
}),
},
resolve: (_root, args, ctx) => {
if (!ctx.currentUser) {
throw new Error("You must be logged in to update a bookmark");
}
const existing = ctx.db.bookmarks.findById(args.id);
if (!existing || existing.userId !== ctx.currentUser.id) {
throw new Error("Bookmark not found or not authorized");
}
const validated = UpdateBookmarkInput.parse({
url: args.input.url,
title: args.input.title,
description: args.input.description,
tags: args.input.tags,
});
return ctx.db.bookmarks.update(args.id, validated);
},
})
);
// Mutation : supprimer un favori
builder.mutationField("deleteBookmark", (t) =>
t.field({
type: "Boolean",
args: {
id: t.arg.string({ required: true }),
},
resolve: (_root, args, ctx) => {
if (!ctx.currentUser) {
throw new Error("You must be logged in to delete a bookmark");
}
const existing = ctx.db.bookmarks.findById(args.id);
if (!existing || existing.userId !== ctx.currentUser.id) {
throw new Error("Bookmark not found or not authorized");
}
return ctx.db.bookmarks.delete(args.id);
},
})
);Chaque mutation vérifie ctx.currentUser avant de continuer. C'est l'équivalent GraphQL d'un middleware — vous contrôlez l'accès au niveau du résolveur.
Les schémas Zod valident les entrées à l'exécution. Si un client envoie une URL invalide ou un titre trop long, il reçoit un message d'erreur clair.
Étape 8 : Assembler le schéma
Créez graphql/schema.ts pour tout rassembler :
import { builder } from "./builder";
// Importer les types pour les enregistrer avec le constructeur
import "./types/user";
import "./types/bookmark";
// Construire et exporter le schéma exécutable
export const schema = builder.toSchema();Les imports ont des effets secondaires — ils enregistrent les types avec le constructeur au chargement du module. C'est un pattern courant dans Pothos.
Étape 9 : Créer le route handler GraphQL
Exposez maintenant le schéma comme route API Next.js. Créez app/api/graphql/route.ts :
import { createYoga } from "graphql-yoga";
import { schema } from "@/graphql/schema";
import { createContext } from "@/graphql/context";
const yoga = createYoga({
schema,
context: ({ request }) => createContext(request),
graphqlEndpoint: "/api/graphql",
graphiql: process.env.NODE_ENV === "development",
fetchAPI: { Response },
});
const handler = yoga;
export { handler as GET, handler as POST };C'est tout ce qu'il vous faut. GraphQL Yoga gère l'analyse des requêtes, la validation, l'exécution, la gestion des erreurs et le playground GraphiQL en développement.
Étape 10 : Tester avec GraphiQL
Démarrez le serveur de développement :
npm run devOuvrez http://localhost:3000/api/graphql dans votre navigateur. Vous devriez voir le playground GraphiQL. Essayez ces requêtes :
Lister tous les favoris :
query {
bookmarks {
id
title
url
tags
user {
name
}
}
}Rechercher des favoris :
query {
bookmarks(search: "react") {
title
url
description
}
}Filtrer par tag :
query {
bookmarks(tag: "graphql") {
title
tags
}
}Pour les mutations, ajoutez l'en-tête x-user-id dans le panneau des en-têtes de GraphiQL :
{
"x-user-id": "1"
}Puis exécutez :
mutation {
createBookmark(input: {
url: "https://yoga-graphql.com"
title: "GraphQL Yoga"
description: "A fully-featured GraphQL server"
tags: ["graphql", "server"]
}) {
id
title
url
tags
createdAt
}
}Étape 11 : Configurer le client urql
Construisez maintenant un frontend React pour consommer l'API. Créez lib/urql.ts :
import { cacheExchange, createClient, fetchExchange } from "@urql/next";
export function makeClient() {
return createClient({
url: "/api/graphql",
exchanges: [cacheExchange, fetchExchange],
fetchOptions: () => ({
headers: {
"x-user-id": "1",
},
}),
});
}Créez un composant provider dans app/providers.tsx :
"use client";
import { UrqlProvider, ssrExchange } from "@urql/next";
import { useMemo } from "react";
import { makeClient } from "@/lib/urql";
export function Providers({ children }: { children: React.ReactNode }) {
const [client, ssr] = useMemo(() => {
const ssr = ssrExchange({ isClient: typeof window !== "undefined" });
const client = makeClient();
return [client, ssr];
}, []);
return (
<UrqlProvider client={client} ssr={ssr}>
{children}
</UrqlProvider>
);
}Étape 12 : Construire la page des favoris
Créez app/bookmarks/page.tsx :
"use client";
import { useQuery, useMutation } from "@urql/next";
import { gql } from "graphql-tag";
import { useState } from "react";
const BOOKMARKS_QUERY = gql`
query Bookmarks($search: String, $tag: String) {
bookmarks(search: $search, tag: $tag) {
id
title
url
description
tags
createdAt
user {
name
}
}
}
`;
const CREATE_BOOKMARK = gql`
mutation CreateBookmark($input: CreateBookmarkInput!) {
createBookmark(input: $input) {
id
title
url
tags
}
}
`;
const DELETE_BOOKMARK = gql`
mutation DeleteBookmark($id: String!) {
deleteBookmark(id: $id)
}
`;
export default function BookmarksPage() {
const [search, setSearch] = useState("");
const [selectedTag, setSelectedTag] = useState("");
const [result, reexecute] = useQuery({
query: BOOKMARKS_QUERY,
variables: {
search: search || null,
tag: selectedTag || null,
},
});
const [, createBookmark] = useMutation(CREATE_BOOKMARK);
const [, deleteBookmark] = useMutation(DELETE_BOOKMARK);
const { data, fetching, error } = result;
async function handleCreate(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = new FormData(e.currentTarget);
await createBookmark({
input: {
url: form.get("url") as string,
title: form.get("title") as string,
description: (form.get("description") as string) || null,
tags: (form.get("tags") as string)
.split(",")
.map((t) => t.trim())
.filter(Boolean),
},
});
e.currentTarget.reset();
reexecute({ requestPolicy: "network-only" });
}
async function handleDelete(id: string) {
await deleteBookmark({ id });
reexecute({ requestPolicy: "network-only" });
}
const allTags = [
...new Set(data?.bookmarks?.flatMap((b: any) => b.tags) ?? []),
];
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">Favoris</h1>
<div className="flex gap-4 mb-6">
<input
type="text"
placeholder="Rechercher des favoris..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="flex-1 px-4 py-2 border rounded-lg"
/>
<select
value={selectedTag}
onChange={(e) => setSelectedTag(e.target.value)}
className="px-4 py-2 border rounded-lg"
>
<option value="">Tous les tags</option>
{allTags.map((tag) => (
<option key={tag} value={tag}>
{tag}
</option>
))}
</select>
</div>
<form onSubmit={handleCreate} className="mb-8 p-4 bg-gray-50 rounded-lg">
<h2 className="text-lg font-semibold mb-4">Ajouter un favori</h2>
<div className="grid grid-cols-2 gap-4">
<input name="url" placeholder="URL" required className="px-3 py-2 border rounded" />
<input name="title" placeholder="Titre" required className="px-3 py-2 border rounded" />
<input name="description" placeholder="Description (optionnel)" className="px-3 py-2 border rounded" />
<input name="tags" placeholder="Tags (séparés par des virgules)" className="px-3 py-2 border rounded" />
</div>
<button type="submit" className="mt-4 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
Ajouter
</button>
</form>
{fetching && <p>Chargement...</p>}
{error && <p className="text-red-500">Erreur : {error.message}</p>}
{data?.bookmarks && (
<div className="space-y-4">
{data.bookmarks.map((bookmark: any) => (
<div key={bookmark.id} className="p-4 border rounded-lg hover:shadow-md transition-shadow">
<div className="flex justify-between items-start">
<div>
<a
href={bookmark.url}
target="_blank"
rel="noopener noreferrer"
className="text-lg font-semibold text-blue-600 hover:underline"
>
{bookmark.title}
</a>
<p className="text-sm text-gray-500 mt-1">{bookmark.url}</p>
{bookmark.description && (
<p className="text-gray-700 mt-2">{bookmark.description}</p>
)}
<div className="flex gap-2 mt-2">
{bookmark.tags.map((tag: string) => (
<span
key={tag}
className="px-2 py-1 bg-gray-200 rounded-full text-xs cursor-pointer hover:bg-gray-300"
onClick={() => setSelectedTag(tag)}
>
{tag}
</span>
))}
</div>
<p className="text-xs text-gray-400 mt-2">
par {bookmark.user?.name} · {new Date(bookmark.createdAt).toLocaleDateString("fr")}
</p>
</div>
<button
onClick={() => handleDelete(bookmark.id)}
className="text-red-500 hover:text-red-700 text-sm"
>
Supprimer
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}Étape 13 : Ajouter la gestion des erreurs
GraphQL a un système d'erreurs intégré. Créez graphql/errors.ts :
import { GraphQLError } from "graphql";
export class AuthenticationError extends GraphQLError {
constructor(message = "You must be logged in") {
super(message, {
extensions: { code: "UNAUTHENTICATED" },
});
}
}
export class AuthorizationError extends GraphQLError {
constructor(message = "You are not authorized to perform this action") {
super(message, {
extensions: { code: "FORBIDDEN" },
});
}
}
export class NotFoundError extends GraphQLError {
constructor(resource: string) {
super(`${resource} not found`, {
extensions: { code: "NOT_FOUND" },
});
}
}Le champ extensions.code permet au client de gérer les erreurs de manière programmatique :
if (error.graphQLErrors.some((e) => e.extensions?.code === "UNAUTHENTICATED")) {
// Rediriger vers la connexion
}Étape 14 : Ajouter la pagination
Pour les API de production, vous avez besoin de pagination. Ajoutez une pagination par curseur :
query {
paginatedBookmarks(first: 2) {
edges {
cursor
node {
title
url
}
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}Pour la page suivante, passez after: "last-cursor-value".
Dépannage
"Cannot find module @/graphql/schema"
Vérifiez que votre tsconfig.json a l'alias de chemin @/* configuré :
{
"compilerOptions": {
"paths": {
"@/*": ["./*"]
}
}
}GraphiQL ne se charge pas
GraphiQL est activé uniquement en mode développement. Vérifiez que NODE_ENV n'est pas défini sur production.
Erreurs "You must be logged in"
Pour les mutations, ajoutez l'en-tête x-user-id: 1 dans GraphiQL ou dans la configuration de votre client urql.
Prochaines étapes
Vous avez maintenant une API GraphQL entièrement typesafe avec Next.js. Voici comment l'étendre :
- Ajouter une vraie base de données — Remplacez le stockage en mémoire par Prisma ou Drizzle ORM connecté à PostgreSQL
- Implémenter l'authentification — Utilisez NextAuth.js ou Better Auth pour émettre de vrais tokens
- Ajouter des abonnements — GraphQL Yoga supporte les abonnements WebSocket pour les mises à jour en temps réel
- Générer les types client — Utilisez
graphql-codegenpour générer des hooks typés pour votre frontend - Déployer — GraphQL Yoga fonctionne sur Vercel, Cloudflare Workers et toute plateforme Node.js
Conclusion
Dans ce tutoriel, vous avez construit une API GraphQL complète avec Next.js App Router, GraphQL Yoga et Pothos. Vous avez appris à :
- Définir un schéma code-first avec une inférence TypeScript complète grâce à Pothos
- Configurer GraphQL Yoga comme route handler Next.js
- Construire des requêtes avec filtrage et des mutations avec validation Zod
- Implémenter l'authentification via le contexte et la gestion structurée des erreurs
- Ajouter une pagination par curseur pour un usage en production
- Connecter un frontend React avec urql pour une récupération de données typesafe
La combinaison de Pothos et GraphQL Yoga vous offre le meilleur des deux mondes : la flexibilité de GraphQL avec la sécurité des types TypeScript — sans génération de code, sans fichiers SDL, juste du TypeScript de bout en bout.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Créer des APIs Type-Safe de bout en bout avec tRPC et Next.js App Router
Apprenez à créer des APIs entièrement type-safe avec tRPC et Next.js 15 App Router. Ce tutoriel pratique couvre la configuration du routeur, les procédures, le middleware, l'intégration de React Query et les appels côté serveur — le tout sans écrire un seul schéma d'API.

Construire une Application Full-Stack avec Drizzle ORM et Next.js 15 : Base de Donnees Type-Safe du Zero a la Production
Apprenez a construire une application full-stack type-safe avec Drizzle ORM et Next.js 15. Ce tutoriel pratique couvre la conception de schemas, les migrations, les Server Actions, les operations CRUD et le deploiement avec PostgreSQL.

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.