# Créer un formulaire de feedback dans l'application avec Next.js

> Un formulaire de feedback dans une application Next.js : une server action qui lit la session, tire la page de la requête et transmet une note avec l'utilisateur joint.

Publié le 2026-10-25 · https://ikiform.com/fr/guides/feedback-form/nextjs

Placez un petit formulaire client avec une note de 1 à 5 sur les écrans où vous voulez du feedback, et reliez-le à une server action. L'action lit l'utilisateur connecté dans votre cookie de session, prend la page dans l'en-tête `Referer` de la requête et transmet le tout à votre backend de formulaires avec une clé secrète. Le navigateur n'envoie jamais l'identité de l'utilisateur : personne ne peut donc envoyer un feedback au nom de quelqu'un d'autre.

L'[onglet Next.js du guide principal](https://ikiform.com/fr/guides/feedback-form.md#form) est un formulaire de feedback de page pour tout visiteur. Cette page concerne l'intérieur de votre application, là où les gens sont connectés. Vous savez déjà qui écrit et sur quel écran : le formulaire pose donc deux questions et le serveur complète le reste. La configuration du produit, les clés et les domaines autorisés sont dans le [guide Next.js de la documentation](https://ikiform.com/fr/docs/guides/nextjs.md).

## Ce qui change quand l'utilisateur est connecté

Un formulaire de feedback public prend ce que le navigateur lui donne. Un formulaire dans l'application ne devrait pas. Si le navigateur envoie `user_id`, n'importe qui peut le changer dans les outils de développement et envoyer un feedback au nom d'un collègue ou d'un client. Chaque valeur doit venir de la source qui mérite confiance pour elle :

| Valeur | Provient de | Pourquoi là |
| --- | --- | --- |
| `rating`, `comment` | Le formulaire | Seul l'utilisateur les connaît |
| `user_id`, `email` | Le cookie de session, lu dans l'action | Le navigateur ne peut pas falsifier une session signée |
| `page` | L'en-tête `Referer` de la requête de l'action | Une server action envoie à la page qui l'affiche, donc l'en-tête contient l'URL complète |
| `screen` | Une prop transmise par la page, comme `projects/[id]/settings` | Regroupe la page de réglages de chaque projet sous un même nom, pour que les moyennes aient du sens |

`screen` est la seule valeur qui passe par le navigateur. C'est une étiquette de regroupement, pas une permission, et l'action vérifie sa forme avant qu'elle ne soit stockée.

Schéma : Comment une réponse de feedback Next.js récupère l'utilisateur et la page côté serveur

1. L'utilisateur choisit une note et écrit éventuellement un commentaire. Le formulaire les envoie avec le nom de l'écran.
2. La server action s'exécute comme un POST vers la page du formulaire, avec le cookie de session et la page dans Referer.
3. getUser() lit et vérifie le cookie de session. Sans session valide, rien n'est envoyé.
4. L'action ajoute l'id et l'e-mail de l'utilisateur ainsi que l'adresse de la page, et envoie à Ikiform avec la clé secrète.
5. La réponse d'Ikiform devient l'état de l'action : la ligne de remerciement, ou l'erreur avec le commentaire toujours saisi.

La session est lue à l'envoi du formulaire, pas au rendu de la page. La page autour du formulaire peut rester statique.

## Mise en place du projet

Quatre fichiers, dont un que vous avez probablement déjà. Le layout de l'App Router est le layout habituel :

`Arborescence du projet`:

```text
app/
  (app)/
    projects/[id]/settings/page.tsx
  feedback/
    actions.ts
    feedback-form.tsx
lib/
  user.ts
.env.local
```

Créez une clé secrète sous **API keys** avec le scope `submissions:write` et placez-la dans `.env.local`. Ne lui donnez jamais de préfixe `NEXT_PUBLIC_`, qui l'enverrait à tous les navigateurs.

`.env.local`:

```bash
IKIFORM_SECRET_KEY=ik_sk_your_secret_key
SESSION_SECRET=the-secret-your-auth-already-uses
```

Dans l'onglet **Fields** du formulaire, collez cette définition. Les utilisateurs voient les deux premiers champs ; votre serveur renseigne les quatre autres. Comme il y a un champ `email`, **Reply** sur la notification écrit directement à l'utilisateur.

Formulaire : In-app feedback

- **Comment cet écran fonctionne-t-il pour vous ?** (`rating`, rating, obligatoire)
- **Qu'est-ce qui l'améliorerait ?** (`comment`, textarea)
- **Page** (`page`, url)
- **Écran** (`screen`, text)
- **Utilisateur** (`user_id`, text)
- **E-mail** (`email`, email)

Essayez une note de `0` ou un nom de champ inconnu : l'aperçu répond avec le même `422` que reçoit votre action.

## La construire

### Lire l'utilisateur à un seul endroit

Next.js recommande une couche d'accès aux données : une seule fonction réservée au serveur qui lit et vérifie la session, utilisée par chaque action. Le `cache` de React fait qu'au sein d'une même requête, les appels répétés ne lisent le cookie qu'une fois. `decrypt` est ce que votre code de session a déjà ; avec une bibliothèque d'authentification, renvoyez plutôt son utilisateur ici.

`lib/user.ts`:

```ts
import "server-only";

import { cookies } from "next/headers";
import { cache } from "react";
import { decrypt } from "@/lib/session";

export interface User {
  id: string;
  email: string;
}

export const getUser = cache(async (): Promise<User | null> => {
  const token = (await cookies()).get("session")?.value;
  if (!token) return null;
  const session = await decrypt(token);
  return session?.userId ? { id: session.userId, email: session.email } : null;
});
```

### La server action

`app/feedback/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";
import { getUser } from "@/lib/user";

export interface FeedbackState {
  status: "idle" | "sent" | "error";
  message: string;
  comment?: string;
}

const SCREEN = /^[a-z0-9/[\]._-]{1,120}$/;

function pageFrom(referer: string | null) {
  try {
    const url = new URL(referer ?? "");
    return url.origin + url.pathname;
  } catch {
    return "";
  }
}

export async function sendFeedback(_previous: FeedbackState, formData: FormData): Promise<FeedbackState> {
  const comment = String(formData.get("comment") ?? "").trim().slice(0, 4000);
  const user = await getUser();
  if (!user) return { status: "error", message: "Your session ended. Sign in again, then send it.", comment };

  const rating = Number(formData.get("rating"));
  if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
    return { status: "error", message: "Pick a score from 1 to 5.", comment };
  }

  const request = await headers();
  const screen = String(formData.get("screen") ?? "");

  let response: Response;
  try {
    response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
        Origin: request.get("origin") ?? "",
      },
      body: JSON.stringify({
        rating,
        comment,
        page: pageFrom(request.get("referer")),
        screen: SCREEN.test(screen) ? screen : "unknown",
        user_id: user.id,
        email: user.email,
        _gotcha: String(formData.get("_gotcha") ?? ""),
      }),
      signal: AbortSignal.timeout(8000),
    });
  } catch {
    return { status: "error", message: "We couldn't send it. Try again in a moment.", comment };
  }

  const result = await response.json().catch(() => null);
  if (result?.ok) return { status: "sent", message: "Thanks. We read every one." };
  return {
    status: "error",
    message: result?.error?.fields?.comment?.[0] ?? result?.error?.message ?? "We couldn't send it. Try again.",
    comment,
  };
}
```

- La vérification de session passe en premier et ne lève pas d'exception. Un utilisateur dont la session a expiré en plein commentaire reçoit un message et garde son commentaire, pas une page d'erreur.
- `pageFrom` ne garde que l'origine et le chemin de `Referer`. Dans une application, les paramètres d'URL contiennent souvent des id et des tokens que vous ne voulez pas dans une boîte de feedback.
- `screen` doit correspondre à un court motif en forme de chemin. Tout le reste est stocké comme `unknown`, pour qu'un envoi forgé ne puisse pas remplir la colonne de déchets.
- `Origin` transmet l'origine du navigateur. En production, c'est votre domaine ; sur votre machine, c'est `localhost`, ce qui rend les tests locaux gratuits, comme expliqué plus bas.
- `AbortSignal.timeout` évite que le bouton tourne indéfiniment quand le réseau est lent.

### Le formulaire

`app/feedback/feedback-form.tsx`:

```tsx
"use client";

import { useActionState } from "react";
import { type FeedbackState, sendFeedback } from "./actions";

const initial: FeedbackState = { status: "idle", message: "" };
const SCORES = ["Broken", "Frustrating", "Fine", "Good", "Great"];

export function FeedbackForm({ screen }: { screen: string }) {
  const [state, action, pending] = useActionState(sendFeedback, initial);

  if (state.status === "sent") return <p role="status">{state.message}</p>;

  return (
    <form action={action} aria-busy={pending}>
      <fieldset>
        <legend>How is this screen working for you?</legend>
        {SCORES.map((label, index) => (
          <label key={label}>
            <input type="radio" name="rating" value={index + 1} required /> {label}
          </label>
        ))}
      </fieldset>
      <label htmlFor="feedback-comment">What would make it better?</label>
      <textarea id="feedback-comment" name="comment" rows={4} maxLength={4000} defaultValue={state.comment} />
      <input type="hidden" name="screen" value={screen} />
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.status === "error" ? <p role="alert">{state.message}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Send feedback"}</button>
    </form>
  );
}
```

- L'action va directement dans `action`. Next.js l'affiche comme un vrai envoi de formulaire, donc un utilisateur qui clique avant le chargement du JavaScript voit quand même son feedback envoyé.
- `pending` de `useActionState` désactive le bouton pendant l'exécution de l'action, ce qui évite les doubles envois.
- React réinitialise le formulaire après une action. `defaultValue={state.comment}` remet le commentaire en cas d'erreur, pour que personne ne le tape deux fois.
- Les libellés nomment les notes. `Broken` et `Great` en disent plus que `1` et `5`, et la valeur stockée reste un nombre.

### Le placer sur un écran

`app/(app)/projects/[id]/settings/page.tsx`:

```tsx
import { FeedbackForm } from "@/app/feedback/feedback-form";

export default function ProjectSettingsPage() {
  return (
    <main>
      <h1>Project settings</h1>
      <aside>
        <FeedbackForm screen="projects/[id]/settings" />
      </aside>
    </main>
  );
}
```

Passez le chemin du dossier de la route comme `screen`. La page de réglages de chaque projet partage alors un même nom, et l'adresse complète avec le vrai id reste dans `page`.

## Honeypot, vérification humaine et limites

L'input masqué `_gotcha` est transmis tel quel. Ikiform stocke un honeypot rempli comme spam. La vérification humaine ne s'exécute pas, car un envoi avec une clé secrète la contourne, et ça convient ici : l'action refuse toute personne sans session, ce qu'un bot n'a pas. Le risque restant est un compte connecté qui envoie en boucle. La limite de débit d'Ikiform ne voit que l'adresse de votre serveur : choisissez donc un préréglage adapté.

Les versions côté serveur envoient avec une clé secrète, donc Ikiform ignore **Allowed domains** et sa vérification humaine. Chaque envoi vient alors de l'IP de votre serveur, et la limite Standard de 5 par minute et par visiteur compte tous vos visiteurs ensemble. Choisissez la [limite de débit](https://ikiform.com/fr/docs/rate-limits.md) Relaxed ou Custom pour un formulaire que vous envoyez via un serveur, et gardez `IKIFORM_SECRET_KEY` dans des variables d'environnement réservées au serveur.

## Rendu et cache

Le formulaire ne lit rien au rendu. Il n'appelle ni `cookies()` ni `headers()`, il ne rend donc pas la page dynamique, et avec Cache Components activé il peut rester dans le shell statique sans frontière `<Suspense>`. Seule l'action touche à la requête, et les actions s'exécutent toujours à chaque requête. Si la page lit déjà la session pour d'autres raisons, rien ne change.

## Déployer

| Hébergeur | Que faire |
| --- | --- |
| Vercel | Ajoutez `IKIFORM_SECRET_KEY` dans les **Environment Variables** du projet pour Production et Preview. Les actions s'exécutent comme des fonctions, sans configuration supplémentaire. |
| Netlify | Netlify build Next.js avec sa propre intégration. Ajoutez la clé dans les variables d'environnement du site avec le scope Functions, puis redéployez. |
| Cloudflare | Cloudflare exécute Next.js sur Workers via son intégration OpenNext. Stockez la clé comme secret du Worker, et mettez-la dans `.dev.vars` pour les aperçus locaux du build Worker. |

Derrière un proxy ou un CDN qui change l'hôte, Next.js rejette les actions dont l'`Origin` ne correspond pas. Listez ce domaine dans `experimental.serverActions.allowedOrigins` de `next.config.ts`.

## Tester gratuitement sur localhost

Un espace de travail sans offre reçoit 50 soumissions de test par mois, mais seulement depuis `localhost`, qu'Ikiform lit dans l'en-tête `Origin`. Un `fetch` côté serveur n'envoie pas d'`Origin` de lui-même : sans la ligne qui le transmet, chaque test local reviendrait en `402 plan_required` et serait retenu. Avec elle, `next dev` sur `http://localhost:3000` compte comme un test local, et les envois de production portent votre vrai domaine.

## Vérifiez que tout fonctionne

1. Lancez `next dev`, connectez-vous, ouvrez les réglages d'un projet et envoyez une note de 2 avec un commentaire.
2. Ouvrez **Submissions**. La réponse a `user_id`, `email`, `screen` à `projects/[id]/settings`, et `page` avec le vrai id du projet, sans paramètres d'URL.
3. Déconnectez-vous dans un autre onglet, puis envoyez à nouveau. Le formulaire affiche le message de connexion, avec votre commentaire toujours dans le champ.
4. Désactivez JavaScript et envoyez encore une fois. La page se recharge avec la ligne de remerciement.
5. Répondez à l'e-mail de notification. La réponse part à l'adresse de l'utilisateur connecté.

## Pièges

| Problème | Que faire |
| --- | --- |
| `402 plan_required` à chaque test local | L'envoi est arrivé à Ikiform sans origine `localhost`. Vérifiez la ligne `Origin` dans l'action, et ouvrez l'application sur `localhost`, pas sur l'adresse réseau de votre machine. |
| `page` est vide | Une `Referrer-Policy` stricte comme `no-referrer` supprime l'en-tête, même sur les requêtes de même origine. Utilisez `strict-origin-when-cross-origin`, la valeur par défaut. |
| Failed to find Server Action, juste après un déploiement | Un onglet exécute encore le build précédent, dont l'id d'action n'existe plus. Demandez à l'utilisateur de recharger, et gardez la même `NEXT_SERVER_ACTIONS_ENCRYPTION_KEY` sur toutes les instances si vous auto-hébergez. |
| `403 forbidden` de la part d'Ikiform | La clé n'a pas `submissions:write`, appartient à un autre espace de travail ou a été révoquée. Créez-en une nouvelle. |
| `422 validation_failed` | L'envoi ne correspond pas à la définition des champs du formulaire. `error.fields` nomme chaque champ et la raison. Faites correspondre le `name` de l'input à l'id du champ. |

## Questions

### Comment joindre l'utilisateur connecté au feedback dans Next.js ?

Lisez la session dans la server action, pas dans le navigateur. Appelez-y votre helper de session ou d'authentification et ajoutez l'id et l'e-mail de l'utilisateur à l'envoi. Tout ce que le navigateur envoie sur l'identité peut être modifié par l'utilisateur.

### Comment une server action Next.js sait-elle de quelle page vient le feedback ?

Une server action est un POST vers la page qui affiche le formulaire, donc son en-tête Referer contient l'URL de cette page. Gardez l'origine et le chemin et supprimez les paramètres d'URL.

### Un formulaire de feedback rend-il ma page Next.js dynamique ?

Pas celui-ci. Le formulaire ne lit ni cookies ni en-têtes pendant le rendu. Seule l'action lit la session, et elle s'exécute à chaque envoi.

### Ikiform est-il gratuit ?

Non, il n'y a pas d'offre gratuite. Les tests sont gratuits : vous pouvez envoyer 50 soumissions de test par mois depuis localhost avant de payer. Pour passer en production, Pro commence à 6 $/mois pour 500 soumissions, ou vous pouvez payer une seule fois 10 000 soumissions pour 49 $ ou 50 000 soumissions pour 149 $, avec des crédits qui n'expirent jamais.

## Autres guides

- [Formulaire de feedback pour un site ou une application](https://ikiform.com/fr/guides/feedback-form.md): Placez un formulaire de feedback sur n'importe quelle page ou dans votre application : une note, un commentaire, la page d'origine, le filtrage du spam et des alertes quand une note est basse.
- [Widget de feedback React](https://ikiform.com/fr/guides/feedback-form/react-feedback-widget.md): Un bouton de feedback flottant pour une application React qui ouvre un petit panneau, envoie l'écran, la version et le compte avec chaque réponse, et ne perd jamais un brouillon.
- [Formulaire de feedback React Router](https://ikiform.com/fr/guides/feedback-form/react-router.md): Un formulaire de feedback React Router 8 : l'action d'une resource route lit la session cookie, un fetcher envoie depuis le layout et un envoi sans JS redirige en retour.
- [Formulaire de feedback TanStack Start](https://ikiform.com/fr/guides/feedback-form/tanstack-start.md): Un formulaire de feedback dans une application TanStack Start : une server function POST avec un middleware de session, l'id de route comme contexte et la page lue dans les en-têtes.
- [Formulaire de signalement de bug](https://ikiform.com/fr/guides/feedback-form/bug-report.md): Un formulaire de signalement de bug qui demande les étapes et la gravité, et laisse la page ajouter elle-même l'URL, le navigateur, le viewport et les erreurs récentes.
- [Formulaire de demande de fonctionnalité](https://ikiform.com/fr/guides/feedback-form/feature-request.md): Un formulaire de demande de fonctionnalité qui demande le problème plutôt que la solution, range les demandes par thème, compte les personnes et non les lignes, et les prévient quand c'est livré.
