Prerequisites
Before you start, you need:- A formcarry account. Sign up is free.
- A form in the dashboard. Its endpoint is on the form’s Setup page. The examples use
https://formcarry.com/s/AbC123xyz; put yours in its place.
The component
To send a form from the App Router, mark the file as a client component, post the fields as JSON, and sendAccept: application/json so the answer comes back as JSON:
email key is the visitor’s address, so it becomes the reply-to address of your notification email and the recipient of the auto response. A stored submission answers with code: 200. A refused one answers with the reason in message, see What every form needs.
The same request with Axios is on the React page.
"use client" on the first line rather than leaving it out, otherwise the App Router renders the file as a Server Component, where useState is not available. In the Pages Router the same component works without that line.
To read the endpoint from the environment instead of the source, put it in .env.local with the NEXT_PUBLIC_ prefix:
NEXT_PUBLIC_FORMCARRY_ENDPOINT rather than FORMCARRY_ENDPOINT, otherwise the browser reads undefined and the request never reaches formcarry. Next.js inlines only NEXT_PUBLIC_ variables into the browser bundle, and this request runs in the browser.
Files
To send files, build aFormData from the form element in the client component and leave the content type to the browser:
name, including the file input, because FormData reads them by name. Send Accept and nothing else rather than adding Content-Type yourself, otherwise the multipart boundary is missing and the upload fails.
For several files, or for File objects that a dropzone library hands you, append each one under its own name:
FileReader, and give each file its own key (never an array):
Files are stored on paid plans only. Free plans store the rest of the submission without them.
Server Action
A Server Action posts from your server, so the endpoint stays out of the browser bundle. Put the endpoint in.env.local without the prefix:
"use server":
fetch:
NEXT_PUBLIC_ prefix. The endpoint is not a secret. Any HTML form that posts to it already shows the endpoint in its action, so posting from the server doesn’t make it any safer.
Because of this, every submission now leaves from your server’s IP address. The rate limit (1 submission per 15 seconds per form per IP address) applies to all of your visitors together as one shared limit. If two users submit within 15 seconds of each other, the second gets a 429. The spam blocker token is still produced in the browser, so the client component still renders the challenge and passes the token to the action.
Spam blocker
To add a challenge, render its widget in the client component, send the token under the name formcarry reads, and paste the secret key into the form’s settings under Form Security:
With reCAPTCHA v2, put the site key in
.env.local as NEXT_PUBLIC_RECAPTCHA_SITE_KEY, read it the same way as the endpoint, and send nothing while the token is empty:
executeRecaptcha() when the visitor submits, and send what it returns:
onVerify:
onSuccess:
403 for reCAPTCHA and 400 for the others. Add localhost to the challenge’s allowed domains while you test.
Validation errors
A422 carries errors, one entry per failing field, each with a message. To show each one next to its field, keep errors in state and check for 422 before the general error:
What’s next
- Field validations: the rules you can set per field.
- Spam protection: the challenges, the filter and the honeypot.
- Email notifications: the auto response the visitor gets, keyed on
email.