Next.js forms to Excel
A route handler, a fetch, or a plain form action - every submission lands as a row in an Excel table on OneDrive, with no automation tool in the middle.
When the spreadsheet your team actually works in is an Excel workbook, the standard Next.js advice gets clumsy: collect submissions somewhere else, export a CSV, re-import it, repeat. SheetLink Forms removes the loop. Every form gets a permanent endpoint at https://sheetlinkforms.com/f/{token} that accepts JSON, urlencoded, or multipart POSTs (bodies up to 256KB), and each accepted submission is inserted as a table row in an Excel workbook on your OneDrive via Microsoft Graph - live rows in the real workbook, not an export you shuttle around.
Delivery runs through an asynchronous worker with retries at 5 minutes, 30 minutes, and 2 hours, and every attempt is recorded in a delivery log. JSON keys that match your table's column names map automatically (case and punctuation insensitive), so the field names already in your form state usually just work. Google Sheets is available as a destination too, but this page is about the Excel path - one that, notably, no other hosted form backend (Formspree, Basin, Formcarry) offers natively; the usual workaround is Zapier or Power Automate metering every submission as a task. The full pipeline is on how it works.
The product is free during beta and invite-gated - join the waitlist - and there are no per-submission fees, ever.
The best path for Next.js
The best path in Next.js is a JSON POST from a route handler. Your form component posts to /api/contact, the handler forwards the payload to your SheetLink endpoint, and the endpoint answers {"ok":true,"id":"..."}, which you relay straight back to the client. Running the call server-side means no CORS to think about and a single place to validate or enrich the payload before it becomes a row in the workbook.
Two lighter alternatives are fully supported: a client-side fetch straight from the component (the endpoint is built for cross-origin browser posts - send Accept: application/json to get JSON back instead of the 303 redirect meant for classic HTML forms), or a plain <form action> with no JavaScript at all, which uses the 303-redirect thank-you flow.
1. Connect Excel Online and get your endpoint
In the SheetLink Forms dashboard, connect a Microsoft account via OAuth - Microsoft 365 or a personal Microsoft account with OneDrive both work - then pick the workbook and the table submissions should land in. The table's columns define the row shape: SheetLink Forms syncs column names from the table itself, so create the table with its columns before wiring the form (unlike the Google Sheets destination, there is no header seeding). You get a permanent endpoint of the form https://sheetlinkforms.com/f/{token}.
2. Create the route handler
Use the snippet above in app/api/contact/route.ts. On the Pages Router the same logic goes in pages/api/contact.ts with req.body and res.status().json() - anything that can fetch works. Name the JSON keys after your Excel table's column headers so mapping is automatic; matching is case and punctuation insensitive, and explicit per-field mapping exists in the dashboard for the cases where names differ.
3. Wire your form component to it
In a client component, prevent default on submit and fetch("/api/contact") with a JSON body built from your form state. Relay the endpoint's ok flag into your UI state for success and error rendering. Nothing here couples you to a rendering mode - static, SSR, and ISR pages all post the same way.
4. Or post from the client directly
If you do not want the extra hop, fetch https://sheetlinkforms.com/f/{token} straight from the browser. Send Content-Type: application/json (or an Accept: application/json header with form-encoded bodies) and you get the {"ok":true,"id":"..."} JSON response instead of a 303 redirect. Cross-origin browser posts are supported, and the per-form origin allowlist can pin submissions to your domains.
5. Or skip JavaScript entirely
A plain <form action="https://sheetlinkforms.com/f/{token}" method="POST"> works in any server-rendered page with zero client code. Include the off-screen honeypot input (<input name="_slhp" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px">) and the browser follows a 303 redirect to a thank-you page or your custom URL after submitting.
6. Test and watch the row appear in Excel
Send a test submission, then open the workbook in Excel on the web or the desktop app - it is the same OneDrive file, so the new row is right there, no refresh-and-reimport ritual. The dashboard's delivery log records each attempt, and the docs list the error responses worth handling: 404 unknown token, 410 paused form, 403 origin not allowed, 413 payload too large, 429 rate limited, 400 unreadable body.
FAQ
Route handler or client fetch - which should I use?
Use the route handler when you want server-side validation, enrichment, or a single place to change the destination later. Use a direct client fetch when the form is simple and you want less code - the endpoint is designed for cross-origin browser posts, and the per-form origin allowlist can pin it to your domains. Both receive the same {"ok":true,"id":"..."} response.
Does the Excel table have to exist before I send submissions?
Yes. Rows are inserted into a table object inside the workbook, and that table's columns define the row shape - SheetLink Forms syncs column names from the table rather than seeding headers the way the Google Sheets destination does. Create the table in Excel (select your header cells, Insert -> Table), pick it in the dashboard, and you are set. Add a column later and it syncs too.
Is this a CSV export, or real rows in my workbook?
Real rows. Each submission is written with a Microsoft Graph table-row insert into the workbook on your OneDrive - the same file you open in Excel on the web or the desktop app, so rows are visible the moment they land. Because it is a live workbook, Power BI can connect to it directly for dashboards that update as submissions arrive.
Where does the honeypot go in a JSON post?
Include "_slhp": "" as a key in the JSON body and leave it empty, as the snippet does. The honeypot is the one signal that marks spam outright; if a bot fills it, the submission is marked spam. Everything else suspicious is quarantined for one-click review rather than dropped.
What happens if Microsoft Graph is briefly down?
Acceptance and delivery are decoupled. The endpoint accepts the submission immediately, then an asynchronous worker performs the table-row insert and retries at 5 minutes, 30 minutes, and 2 hours if needed. The delivery log in the dashboard shows the outcome of every attempt, so nothing disappears into a webhook void.
Can the same form write to Google Sheets instead?
Yes - Google Sheets is the other first-class destination, connected with one-click OAuth using the drive.file scope. Pick whichever spreadsheet your team actually works in; the endpoint, the snippet, and the spam screening are identical either way.
Also on: Next.js · React · Plain HTML · Netlify-hosted sites