{"id":31903,"date":"2025-06-05T10:24:24","date_gmt":"2025-06-05T15:24:24","guid":{"rendered":"https:\/\/wpengine.com\/builders\/?p=31903"},"modified":"2025-06-17T11:42:40","modified_gmt":"2025-06-17T16:42:40","slug":"build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms","status":"publish","type":"post","link":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/","title":{"rendered":"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms"},"content":{"rendered":"\n<p>Contact forms are a fundamental touchpoint between site visitors and site owners, enabling customer inquiries, lead generation, and essential feedback that drives engagement.<br>In headless WordPress, this can be tricky since the frontend and backend are separated. &nbsp; This means that you have to figure out a way for the frontend to send that data to your WP backend as securely as possible.&nbsp; &nbsp;<\/p>\n\n\n\n<p>In this article, we\u2019ll discuss implementing a simple contact form in headless WordPress using WPGraphQL, Ninja Forms, and the Next.js App Router.<\/p>\n\n\n\n<p>If you prefer the video format, you can access it here:<\/p>\n\n\n\n<iframe class=\"youtube-video\"  src=\"https:\/\/www.youtube.com\/embed\/vY-EWxtn9mI\" title=\"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms\" frameborder=\"0\" style=\"display: block; margin: auto; width: 100%; height: 400px;\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen><\/iframe>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<p>Before we begin, you should have a basic understanding of the following:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Headless WordPress concepts<br><\/li>\n\n\n\n<li>The WPGraphQL plugin<br><\/li>\n\n\n\n<li>The Next.js App Router<\/li>\n<\/ul>\n\n\n\n<p><\/p>\n\n\n\n<p>This article is not a step-by-step walkthrough, but if you\u2019d like to explore the codebase and follow along, you can clone the <a href=\"https:\/\/github.com\/Fran-A-Dev\/ninja-forms-headlesswp\">example repository<\/a>, which includes a detailed setup guide.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Using Ninja Forms with WPGraphQL<\/h2>\n\n\n\n<p><a href=\"https:\/\/ninjaforms.com\/\">Ninja Forms<\/a> is a flexible and user-friendly form-building plugin for WordPress. It offers a robust set of features out of the box, and its core functionality is free and open source.<\/p>\n\n\n\n<p>To expose Ninja Forms data via GraphQL, we\u2019ll use the<a href=\"https:\/\/github.com\/wp-graphql\/wp-graphql-ninja-forms\"> WPGraphQL for Ninja Forms<\/a> extension. This plugin adds a GraphQL schema for Ninja Forms, allowing queries and mutations for form data.<\/p>\n\n\n\n<p>For this example, we\u2019ll stick with the free tier of Ninja Forms and use its default fields: <strong>Name<\/strong>, <strong>Email<\/strong>, and <strong>Message<\/strong>.<\/p>\n\n\n\n<p><br><\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"464\" src=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.10.52\u202fPM-1024x464.png\" alt=\"\" class=\"wp-image-31906\" srcset=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.10.52\u202fPM-1024x464.png 1024w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.10.52\u202fPM-300x136.png 300w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.10.52\u202fPM-768x348.png 768w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.10.52\u202fPM-1536x696.png 1536w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.10.52\u202fPM.png 1895w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>After downloading the WPGraphQL for Ninja Forms plugin, let&#8217;s test that it works by requesting and submitting data to its API:<br>Requesting form default form data:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"466\" src=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.14.22\u202fPM-1024x466.png\" alt=\"\" class=\"wp-image-31908\" srcset=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.14.22\u202fPM-1024x466.png 1024w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.14.22\u202fPM-300x136.png 300w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.14.22\u202fPM-768x349.png 768w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.14.22\u202fPM-1536x699.png 1536w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.14.22\u202fPM.png 1840w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>Submitting data to the form via mutation:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"470\" src=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.15.22\u202fPM-1024x470.png\" alt=\"\" class=\"wp-image-31909\" srcset=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.15.22\u202fPM-1024x470.png 1024w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.15.22\u202fPM-300x138.png 300w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.15.22\u202fPM-768x352.png 768w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.15.22\u202fPM-1536x705.png 1536w, https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/Screenshot-2025-06-04-at-3.15.22\u202fPM.png 1829w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>You should get the results back in the right-hand pane, with the data expected when you query for it, and the success boolean set to true if the submission was successful.<\/p>\n\n\n\n<p>Stoked, these both work!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Form Submission in <a href=\"https:\/\/nextjs.org\/docs\">Next.js<\/a> App Router<\/h2>\n\n\n\n<p>The next step I took was to create the API route responsible for handling the form submission safely. In App Router, <a href=\"https:\/\/nextjs.org\/docs\/app\/building-your-application\/routing\/route-handlers\">route handlers<\/a> allow you to create custom request handlers for a given route using the Web Request and Response APIs.<\/p>\n\n\n\n<p>We will take advantage of that convention in the <code>app\/api\/contact\/<a href=\"http:\/\/route.ts\">route.ts<\/a> <\/code>file.<\/p>\n\n\n\n<p>This file securely bridges a Next.js frontend with a WordPress backend via GraphQL. In the <code>POST<\/code> handler, the code first reads JSON from the incoming request and expects three properties\u2014<code>name, email,<\/code> and <code>message<\/code>. If any of these fields is missing, it immediately returns a <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/HTTP\/Reference\/Status\/400\"><code>400 response<\/code><\/a> with an error. Once validation passes, the handler constructs a WPGraphQL mutation:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">const<\/span> mutation = `\n<\/span><\/span><span class='shcb-loc'><span>  mutation SubmitForm($input: SubmitFormInput!) {\n<\/span><\/span><span class='shcb-loc'><span>    submitForm(input: $input) {\n<\/span><\/span><span class='shcb-loc'><span>      success\n<\/span><\/span><span class='shcb-loc'><span>      message\n<\/span><\/span><span class='shcb-loc'><span>      errors {\n<\/span><\/span><span class='shcb-loc'><span>        fieldId\n<\/span><\/span><span class='shcb-loc'><span>        message\n<\/span><\/span><span class='shcb-loc'><span>        slug\n<\/span><\/span><span class='shcb-loc'><span>      }\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>`;\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Next, the code issues a fetch call to the WordPress GraphQL endpoint, which is specified via the environment variable <code>NEXT_PUBLIC_GRAPHQL_ENDPOINT<\/code>. In that call, the headers include <code>\"Content-Type\": \"application\/json\"<\/code> and an Authorization header built from another environment variable:<\/p>\n\n\n\n<p><code>Authorization: Bearer ${process.env.WP_AUTH_TOKEN}<\/code><\/p>\n\n\n\n<p>Because <code>WP_AUTH_TOKEN<\/code> is pulled from <code>process.env<\/code>, it ensures that only your Next.js app\u2014holding this secret\u2014can successfully authorize and submit data to the WordPress endpoint.&nbsp;<\/p>\n\n\n\n<p>The request body contains the query and a variables object whose input includes <code>formId: 1<\/code>, an array of field objects mapping each field\u2019s id to value: <code>data.name, value: data.email<\/code>, and <code>value: data.message<\/code>, plus a <code>clientMutationId<\/code> set to <code>\"contact-form-submission\"<\/code>.<\/p>\n\n\n\n<p>When the response arrives, the code calls await <code>wpResponse.json()<\/code>, then checks both<code> wpResponse.ok<\/code> and <code>response.errors<\/code>. If either indicates a failure, it logs the first GraphQL error to the server console and throws an exception. In the success case\u2014when <code>submitForm<\/code> returns something like <code>{ success: true, message: \"\u2026\", errors: [] }<\/code>\u2014the route returns a 200 JSON response:<\/p>\n\n\n\n<p><code>{ \"success\": true, \"message\": \"Form submitted successfully\" }<\/code><\/p>\n\n\n\n<p>Any thrown exception or unexpected condition is caught by the catch block, which logs the error and returns a 500 response with <code>{ error: \"Form submission failed\" }<\/code>. Finally, the file exports a GET handler that always returns a 405 <code>\u201cMethod not allowed\u201d<\/code> response, ensuring only POST requests are processed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Interfacing with the API route via server actions<\/h2>\n\n\n\n<p>Now that we have discussed the API route in <a href=\"http:\/\/route.ts\">route.ts<\/a>, let\u2019s go over the server action responsible for interfacing with that API endpoint.&nbsp;<br><br>The <code>actions.ts<\/code> file implements a server-side action that functions as the middleware between the client-side form submission and the API endpoint.&nbsp;<\/p>\n\n\n\n<p>When a user triggers the form submission, the <code>submitForm<\/code> server action is invoked, which processes the form data and initiates an <code>HTTP POST<\/code> request to the <code>\/api\/contact<\/code> endpoint.&nbsp;<\/p>\n\n\n\n<p>This endpoint, implemented in<code> route.ts<\/code>, then executes the WordPress WPGraphQL mutation through the Ninja Forms API.&nbsp;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rendering the Form on the Client<\/h2>\n\n\n\n<p>The <code>contact-form.tsx<\/code> file implements a client-side form component that leverages <a href=\"https:\/\/nextjs.org\/docs\/app\/api-reference\/components\/form\">Next.js&#8217;s Form API<\/a> and <a href=\"https:\/\/nextjs.org\/docs\/app\/building-your-application\/data-fetching\/server-actions-and-mutations\">server actions <\/a>for handling contact form submissions.&nbsp;<\/p>\n\n\n\n<p>The component is marked with the <code>\"use client\"<\/code> directive, indicating that it runs on the client side and utilizes React&#8217;s <code><a href=\"https:\/\/react.dev\/reference\/react-dom\/hooks\/useFormStatus\">useFormStatus<\/a> <\/code>hook to manage form submission states.<\/p>\n\n\n\n<p>The form implementation consists of two main components.<\/p>\n\n\n\n<p>This is a React component called <code>SubmitButton<\/code> that renders a <code>&lt;button&gt;<\/code> whose appearance and behavior change based on the form\u2019s submission state.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">SubmitButton<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> { pending } = useFormStatus();\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">return<\/span> (\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"xml\"><span class=\"hljs-tag\">      <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"submit\"<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"xml\"><span class=\"hljs-tag\">      <span class=\"hljs-attr\">disabled<\/span>=<span class=\"hljs-string\">{pending}<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"xml\"><span class=\"hljs-tag\">      <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">{<\/span>`<span class=\"hljs-attr\">w-full<\/span> <span class=\"hljs-attr\">py-3<\/span> <span class=\"hljs-attr\">px-6<\/span> <span class=\"hljs-attr\">rounded<\/span> <span class=\"hljs-attr\">bg-yellow-500<\/span> <span class=\"hljs-attr\">text-black<\/span> <span class=\"hljs-attr\">font-semibold<\/span> <span class=\"hljs-attr\">hover:bg-yellow-400<\/span> <span class=\"hljs-attr\">transition-colors<\/span> ${<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"xml\"><span class=\"hljs-tag\">        <span class=\"hljs-attr\">pending<\/span> ? \"<span class=\"hljs-attr\">opacity-50<\/span> <span class=\"hljs-attr\">cursor-not-allowed<\/span>\" <span class=\"hljs-attr\">:<\/span> \"\"<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"xml\"><span class=\"hljs-tag\">      }`}<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"xml\"><span class=\"hljs-tag\">    &gt;<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"xml\">      {pending ? \"Sending...\" : \"Send Message\"}<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"xml\">    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span>  );\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And this is the main <code>ContactForm<\/code> component that manages the form state and submission:<br><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">ContactForm<\/span>(<span class=\"hljs-params\"><\/span>) <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">const<\/span> &#91;message, setMessage] = useState&lt;{\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"success\"<\/span> | <span class=\"hljs-string\">\"error\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>    text: string;\n<\/span><\/span><span class='shcb-loc'><span>  } | <span class=\"hljs-literal\">null<\/span>&gt;(<span class=\"hljs-literal\">null<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">handleSubmit<\/span>(<span class=\"hljs-params\">formData: FormData<\/span>) <\/span>{\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> submitForm(formData);\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-keyword\">if<\/span> (<span class=\"hljs-string\">\"error\"<\/span> <span class=\"hljs-keyword\">in<\/span> result) {\n<\/span><\/span><span class='shcb-loc'><span>      setMessage({ <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"error\"<\/span>, <span class=\"hljs-attr\">text<\/span>: result.error });\n<\/span><\/span><span class='shcb-loc'><span>    } <span class=\"hljs-keyword\">else<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>      setMessage({ <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">\"success\"<\/span>, <span class=\"hljs-attr\">text<\/span>: result.success });\n<\/span><\/span><span class='shcb-loc'><span>    }\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>  <span class=\"hljs-comment\">\/\/ ... form JSX<\/span>\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Notice that I decided to use the form component built into <a href=\"http:\/\/next.js\">Next.js<\/a> to keep things simple.&nbsp; You can dynamically fetch the form data from the Ninja Forms WPGraphQL API.&nbsp; For this article and simplicity&#8217;s sake, I chose the static form provided by Next.js<br><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Environment Variables<\/h2>\n\n\n\n<p>Before trying the form on the browser to see if it works, the last thing you have to check is your environment variables.&nbsp; In the <code>.env.local<\/code> file at your project\u2019s root, your environment variables should be as follows:<br><\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span>NEXT_PUBLIC_GRAPHQL_ENDPOINT=<span class=\"hljs-string\">\"https:\/\/your-wpsite.com\/graphql\"<\/span>\n<\/span><\/span><span class='shcb-loc'><span>WP_AUTH_TOKEN=<span class=\"hljs-string\">\"your-auth-token\"<\/span>\n<\/span><\/span><span class='shcb-loc'><span>NEXT_PUBLIC_SITE_URL=http:<span class=\"hljs-comment\">\/\/localhost:3000<\/span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Generate an Auth Token<\/h3>\n\n\n\n<p>You can generate an auth token to add to your environment variable by using <a href=\"https:\/\/docs.openssl.org\/3.4\/man1\/openssl\/\">the openssl command<\/a> in terminal:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span>openssl rand -base64 32\n<\/span><\/span><\/code><\/span><\/pre>\n\n\n<p>This command generates a random 32-byte string encoded in base64, which is great for use as an authentication token. The output is a secure, random string that can be used as the <code>WP_AUTH_TOKEN<\/code> in your environment variables.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Test the form on the browser<\/h2>\n\n\n\n<p>Everything is set up, and now it&#8217;s time to test this form on the browser.&nbsp; I will navigate to my contact form route. Here is the form working in all its glory!<br><\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"3456\" height=\"2232\" src=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/ninjaformsgif.gif\" alt=\"\" class=\"wp-image-31910\"\/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Locking down the edit page on WP Admin<\/h2>\n\n\n\n<p>If you&#8217;re using a static form component in your frontend, any changes made to the form structure in the WordPress admin can break the submission logic.<\/p>\n\n\n\n<p>To prevent this, restrict access to Ninja Forms editing screens by <a href=\"https:\/\/developer.wordpress.org\/plugins\/intro\/\">creating a custom plugin<\/a> that overrides its default capabilities:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Example Plugin: Ninja Forms Admin Lockdown<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-code-table shcb-line-numbers\"><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-meta\">&lt;?php<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\">\/**<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\"> * Plugin Name: Ninja Forms Admin Lockdown<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\"> * Description: Restricts access to all Ninja Forms admin screens so that only users with the 'edit_themes' capability (typically Administrators) can view or modify forms.<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\"> * Version:     1.0.0<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\"> * Author:      Your Name<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\"> * License:     GPLv2 or later<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\"> * Text Domain: ninja-forms-admin-lockdown<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\"> *\/<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\">\/\/ Exit if accessed directly.<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-keyword\">if<\/span> ( ! defined( <span class=\"hljs-string\">'ABSPATH'<\/span> ) ) {<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\">    <span class=\"hljs-keyword\">exit<\/span>;<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\">}<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\">\/**<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\"> * Restrict access to \u201cAll Forms\u201d and the main Ninja Forms menu.<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\"> *<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\"> * By default, Ninja Forms uses 'edit_posts' to gate access.<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\"> * Returning 'edit_themes' here ensures only users with that capability can see or edit.<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-comment\"> *\/<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">nf_allforms_capabilities<\/span><span class=\"hljs-params\">( $cap )<\/span> <\/span>{<\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\">    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">'edit_themes'<\/span>;<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\">}<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\">add_filter( <span class=\"hljs-string\">'ninja_forms_admin_parent_menu_capabilities'<\/span>, <span class=\"hljs-string\">'nf_allforms_capabilities'<\/span> );<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\">add_filter( <span class=\"hljs-string\">'ninja_forms_admin_all_forms_capabilities'<\/span>,   <span class=\"hljs-string\">'nf_allforms_capabilities'<\/span> );<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\"><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\"><span class=\"hljs-comment\">\/**<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\"><span class=\"hljs-comment\"> * Restrict access to \u201cAdd New Form\u201d submenu.<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\"><span class=\"hljs-comment\"> *\/<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">nf_newforms_capabilities<\/span><span class=\"hljs-params\">( $cap )<\/span> <\/span>{<\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\"><span class=\"hljs-function\">    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">'edit_themes'<\/span>;<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\"><span class=\"hljs-function\">}<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\"><span class=\"hljs-function\">add_filter( <span class=\"hljs-string\">'ninja_forms_admin_parent_menu_capabilities'<\/span>, <span class=\"hljs-string\">'nf_newforms_capabilities'<\/span> );<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\"><span class=\"hljs-function\">add_filter( <span class=\"hljs-string\">'ninja_forms_admin_all_forms_capabilities'<\/span>,   <span class=\"hljs-string\">'nf_newforms_capabilities'<\/span> );<\/span><\/span><\/span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"php\"><span class=\"hljs-function\"><span class=\"hljs-function\">add_filter( <span class=\"hljs-string\">'ninja_forms_admin_add_new_capabilities'<\/span>,     <span class=\"hljs-string\">'nf_newforms_capabilities'<\/span> );<\/span><\/span><\/span>\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>In order to do this, you need to hook into Ninja Forms&#8217; capability filters and return a higher-level capability.<\/p>\n\n\n\n<p>Install and activate this plugin to restrict form editing to only site Administrators (those who have the <code>`edit_themes`<\/code> <a href=\"https:\/\/wordpress.org\/documentation\/article\/roles-and-capabilities\/#edit_themes\">capability<\/a>).<br><br><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Integrating a contact form on a headless WordPress site doesn\u2019t have to be complex. By combining <strong>Next.js<\/strong>, <strong>WPGraphQL<\/strong>, and <strong>Ninja Forms<\/strong>, you can build a secure, modern contact form that connects your frontend and backend seamlessly.<\/p>\n\n\n\n<p>As always, we\u2019re super stoked to hear your feedback and learn about the headless projects you\u2019re working on, so hit us up in the <a href=\"https:\/\/wpeng.in\/devrel-discord\/\">Headless WordPress Discord<\/a>!<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Contact forms are a fundamental touchpoint between site visitors and site owners, enabling customer inquiries, lead generation, and essential feedback that drives engagement.In headless WordPress, this can be tricky since [&hellip;]<\/p>\n","protected":false},"author":20,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_EventAllDay":false,"_EventTimezone":"","_EventStartDate":"","_EventEndDate":"","_EventStartDateUTC":"","_EventEndDateUTC":"","_EventShowMap":false,"_EventShowMapLink":false,"_EventURL":"","_EventCost":"","_EventCostDescription":"","_EventCurrencySymbol":"","_EventCurrencyCode":"","_EventCurrencyPosition":"","_EventDateTimeSeparator":"","_EventTimeRangeSeparator":"","_EventOrganizerID":[],"_EventVenueID":[],"_OrganizerEmail":"","_OrganizerPhone":"","_OrganizerWebsite":"","_VenueAddress":"","_VenueCity":"","_VenueCountry":"","_VenueProvince":"","_VenueState":"","_VenueZip":"","_VenuePhone":"","_VenueURL":"","_VenueStateProvince":"","_VenueLat":"","_VenueLng":"","_VenueShowMap":false,"_VenueShowMapLink":false,"footnotes":""},"categories":[23],"tags":[],"class_list":["post-31903","post","type-post","status-publish","format-standard","hentry","category-headless"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.7 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms - Builders<\/title>\n<meta name=\"description\" content=\"Discover how to create a modern, secure contact form in a headless WordPress setup using Next.js App Router, WPGraphQL, and Ninja Forms\u2014plus a plugin to restrict WP Admin form edits.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms\" \/>\n<meta property=\"og:description\" content=\"Learn how to build a headless WordPress contact form using Next.js App Router, WPGraphQL, and Ninja Forms\u2014complete with server\u2010side validation, GraphQL mutations, environment\u2010driven authentication, and locking down the WP Admin form editor for extra security.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/\" \/>\n<meta property=\"og:site_name\" content=\"Builders\" \/>\n<meta property=\"article:published_time\" content=\"2025-06-05T15:24:24+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-06-17T16:42:40+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/WPE-Builders-Contributing-to-\u2028Open-Source-Projects-1920x1080@2x-1024x576.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1024\" \/>\n\t<meta property=\"og:image:height\" content=\"576\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Francis Agulto\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@wpebuilders\" \/>\n<meta name=\"twitter:site\" content=\"@wpebuilders\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Francis Agulto\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/\"},\"author\":{\"name\":\"Francis Agulto\",\"@id\":\"https:\/\/wpengine.com\/builders\/#\/schema\/person\/bcdcb4ac0b215c34b6b30e440a24dc54\"},\"headline\":\"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms\",\"datePublished\":\"2025-06-05T15:24:24+00:00\",\"dateModified\":\"2025-06-17T16:42:40+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/\"},\"wordCount\":1159,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\/\/wpengine.com\/builders\/#organization\"},\"image\":{\"@id\":\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/ninjaformsgif.gif\",\"articleSection\":[\"Headless\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/\",\"url\":\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/\",\"name\":\"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms - Builders\",\"isPartOf\":{\"@id\":\"https:\/\/wpengine.com\/builders\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/ninjaformsgif.gif\",\"datePublished\":\"2025-06-05T15:24:24+00:00\",\"dateModified\":\"2025-06-17T16:42:40+00:00\",\"description\":\"Discover how to create a modern, secure contact form in a headless WordPress setup using Next.js App Router, WPGraphQL, and Ninja Forms\u2014plus a plugin to restrict WP Admin form edits.\",\"breadcrumb\":{\"@id\":\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#primaryimage\",\"url\":\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/ninjaformsgif.gif\",\"contentUrl\":\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/ninjaformsgif.gif\",\"width\":3456,\"height\":2232},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/wpengine.com\/builders\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/wpengine.com\/builders\/#website\",\"url\":\"https:\/\/wpengine.com\/builders\/\",\"name\":\"Builders\",\"description\":\"Reimagining the way we build with WordPress.\",\"publisher\":{\"@id\":\"https:\/\/wpengine.com\/builders\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/wpengine.com\/builders\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/wpengine.com\/builders\/#organization\",\"name\":\"WP Engine\",\"url\":\"https:\/\/wpengine.com\/builders\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/wpengine.com\/builders\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2024\/05\/WP-Engine-Horizontal@2x.png\",\"contentUrl\":\"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2024\/05\/WP-Engine-Horizontal@2x.png\",\"width\":348,\"height\":68,\"caption\":\"WP Engine\"},\"image\":{\"@id\":\"https:\/\/wpengine.com\/builders\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/x.com\/wpebuilders\",\"https:\/\/www.youtube.com\/channel\/UCh1WuL54XFb9ZI6m6goFv1g\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/wpengine.com\/builders\/#\/schema\/person\/bcdcb4ac0b215c34b6b30e440a24dc54\",\"name\":\"Francis Agulto\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/wpengine.com\/builders\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/c24a68b84c9ad2b53c633d14917d8298?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/c24a68b84c9ad2b53c633d14917d8298?s=96&d=mm&r=g\",\"caption\":\"Francis Agulto\"},\"description\":\"Fran Agulto is a Developer Advocate at WP Engine. He is a lover of all things headless WordPress, Rock Climbing, and overall being stoked for people that love what they do and share that stoke with others! Follow me on Twitter for cool stoked headless WP!\",\"url\":\"https:\/\/wpengine.com\/builders\/author\/francis-agultowpengine-com-2-2-2-2-2-2-2-2-2-2-2-3\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms - Builders","description":"Discover how to create a modern, secure contact form in a headless WordPress setup using Next.js App Router, WPGraphQL, and Ninja Forms\u2014plus a plugin to restrict WP Admin form edits.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/","og_locale":"en_US","og_type":"article","og_title":"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms","og_description":"Learn how to build a headless WordPress contact form using Next.js App Router, WPGraphQL, and Ninja Forms\u2014complete with server\u2010side validation, GraphQL mutations, environment\u2010driven authentication, and locking down the WP Admin form editor for extra security.","og_url":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/","og_site_name":"Builders","article_published_time":"2025-06-05T15:24:24+00:00","article_modified_time":"2025-06-17T16:42:40+00:00","og_image":[{"width":1024,"height":576,"url":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/WPE-Builders-Contributing-to-\u2028Open-Source-Projects-1920x1080@2x-1024x576.png","type":"image\/png"}],"author":"Francis Agulto","twitter_card":"summary_large_image","twitter_creator":"@wpebuilders","twitter_site":"@wpebuilders","twitter_misc":{"Written by":"Francis Agulto","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#article","isPartOf":{"@id":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/"},"author":{"name":"Francis Agulto","@id":"https:\/\/wpengine.com\/builders\/#\/schema\/person\/bcdcb4ac0b215c34b6b30e440a24dc54"},"headline":"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms","datePublished":"2025-06-05T15:24:24+00:00","dateModified":"2025-06-17T16:42:40+00:00","mainEntityOfPage":{"@id":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/"},"wordCount":1159,"commentCount":0,"publisher":{"@id":"https:\/\/wpengine.com\/builders\/#organization"},"image":{"@id":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#primaryimage"},"thumbnailUrl":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/ninjaformsgif.gif","articleSection":["Headless"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/","url":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/","name":"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms - Builders","isPartOf":{"@id":"https:\/\/wpengine.com\/builders\/#website"},"primaryImageOfPage":{"@id":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#primaryimage"},"image":{"@id":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#primaryimage"},"thumbnailUrl":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/ninjaformsgif.gif","datePublished":"2025-06-05T15:24:24+00:00","dateModified":"2025-06-17T16:42:40+00:00","description":"Discover how to create a modern, secure contact form in a headless WordPress setup using Next.js App Router, WPGraphQL, and Ninja Forms\u2014plus a plugin to restrict WP Admin form edits.","breadcrumb":{"@id":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#primaryimage","url":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/ninjaformsgif.gif","contentUrl":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2025\/06\/ninjaformsgif.gif","width":3456,"height":2232},{"@type":"BreadcrumbList","@id":"https:\/\/wpengine.com\/builders\/build-a-contact-form-in-headless-wordpress-using-next-js-and-ninja-forms\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/wpengine.com\/builders\/"},{"@type":"ListItem","position":2,"name":"Build a Contact Form in Headless WordPress Using Next.js and Ninja Forms"}]},{"@type":"WebSite","@id":"https:\/\/wpengine.com\/builders\/#website","url":"https:\/\/wpengine.com\/builders\/","name":"Builders","description":"Reimagining the way we build with WordPress.","publisher":{"@id":"https:\/\/wpengine.com\/builders\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/wpengine.com\/builders\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/wpengine.com\/builders\/#organization","name":"WP Engine","url":"https:\/\/wpengine.com\/builders\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/wpengine.com\/builders\/#\/schema\/logo\/image\/","url":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2024\/05\/WP-Engine-Horizontal@2x.png","contentUrl":"https:\/\/wpengine.com\/builders\/wp-content\/uploads\/2024\/05\/WP-Engine-Horizontal@2x.png","width":348,"height":68,"caption":"WP Engine"},"image":{"@id":"https:\/\/wpengine.com\/builders\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/x.com\/wpebuilders","https:\/\/www.youtube.com\/channel\/UCh1WuL54XFb9ZI6m6goFv1g"]},{"@type":"Person","@id":"https:\/\/wpengine.com\/builders\/#\/schema\/person\/bcdcb4ac0b215c34b6b30e440a24dc54","name":"Francis Agulto","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/wpengine.com\/builders\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/c24a68b84c9ad2b53c633d14917d8298?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/c24a68b84c9ad2b53c633d14917d8298?s=96&d=mm&r=g","caption":"Francis Agulto"},"description":"Fran Agulto is a Developer Advocate at WP Engine. He is a lover of all things headless WordPress, Rock Climbing, and overall being stoked for people that love what they do and share that stoke with others! Follow me on Twitter for cool stoked headless WP!","url":"https:\/\/wpengine.com\/builders\/author\/francis-agultowpengine-com-2-2-2-2-2-2-2-2-2-2-2-3\/"}]}},"_links":{"self":[{"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/posts\/31903","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/users\/20"}],"replies":[{"embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/comments?post=31903"}],"version-history":[{"count":0,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/posts\/31903\/revisions"}],"wp:attachment":[{"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/media?parent=31903"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/categories?post=31903"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wpengine.com\/builders\/wp-json\/wp\/v2\/tags?post=31903"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}