[{"data":1,"prerenderedAt":38},["ShallowReactive",2],{"4468638":3},{"type_of":4,"id":5,"title":6,"description":7,"readable_publish_date":8,"slug":9,"path":10,"url":11,"comments_count":12,"public_reactions_count":12,"collection_id":13,"published_timestamp":14,"language":15,"subforem_id":16,"ai_disclosure_level":17,"ai_disclosure_label":18,"positive_reactions_count":12,"cover_image":19,"social_image":20,"canonical_url":11,"created_at":21,"edited_at":13,"crossposted_at":13,"published_at":14,"last_comment_at":14,"reading_time_minutes":22,"tag_list":23,"tags":24,"body_html":29,"body_markdown":30,"user":31},"article",4468638,"Vue 3 Contact Forms Without Writing a Single Line of Backend Code","Vue 3 Contact Forms Without Writing a Single Line of Backend Code   Building a contact form...","Aug 26","vue-3-contact-forms-without-writing-a-single-line-of-backend-code-23b0","/ekwoster/vue-3-contact-forms-without-writing-a-single-line-of-backend-code-23b0","https://dev.to/ekwoster/vue-3-contact-forms-without-writing-a-single-line-of-backend-code-23b0",0,null,"2026-08-26T15:35:00Z","en",1,"not_disclosed","Not Disclosed","https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fft8xck2o84toa1avcklu.png","https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fft8xck2o84toa1avcklu.png","2026-08-23T15:36:05Z",4,"vue, webdev, frontend, staticwebapps",[25,26,27,28],"vue","webdev","frontend","staticwebapps","\u003Ch1>\n  \u003Ca name=\"vue-3-contact-forms-without-writing-a-single-line-of-backend-code\" href=\"#vue-3-contact-forms-without-writing-a-single-line-of-backend-code\">\n  \u003C/a>\n  Vue 3 Contact Forms Without Writing a Single Line of Backend Code\n\u003C/h1>\n\n\u003Cp>Building a contact form in Vue is easy until you get to the part where the submission has to go somewhere. Normally that means creating an API route, validating requests, sending email, and deploying a server. With \u003Ca href=\"https://onsubmit.dev\" target=\"_blank\" rel=\"noopener noreferrer\">onsubmit.dev (form backend)\u003C/a>, you can instead connect a Vue 3 form to a hosted endpoint and route submissions to destinations such as email or Notion.\u003C/p>\n\n\u003Cp>In this tutorial, we'll build a contact form with Vue 3's Composition API and the \u003Ccode>vue-onsubmit\u003C/code> integration.\u003C/p>\n\n\u003Ch2>\n  \u003Ca name=\"why-can-we-skip-the-backend\" href=\"#why-can-we-skip-the-backend\">\n  \u003C/a>\n  Why can we skip the backend?\n\u003C/h2>\n\n\u003Cp>A browser shouldn't send email directly or contain private credentials for third-party APIs such as Notion.\u003C/p>\n\n\u003Cp>A traditional setup therefore looks roughly like this:\u003Cbr>\n\u003C/p>\n\n\u003Cdiv class=\"highlight js-code-highlight\">\n\u003Cpre class=\"highlight plaintext\">\u003Ccode>Vue form\n   ↓\nYour API/server\n   ↓\nEmail / Notion\n\u003C/code>\u003C/pre>\n\u003Cdiv class=\"highlight__panel js-actions-panel\">\n\u003Cdiv class=\"highlight__panel-action js-fullscreen-code-action\">\n    \u003Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"20px\" height=\"20px\" viewbox=\"0 0 24 24\" class=\"highlight-action crayons-icon highlight-action--fullscreen-on\">\u003Ctitle>Enter fullscreen mode\u003C/title>\n    \u003Cpath d=\"M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z\">\u003C/path>\n\u003C/svg>\n\n    \u003Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"20px\" height=\"20px\" viewbox=\"0 0 24 24\" class=\"highlight-action crayons-icon highlight-action--fullscreen-off\">\u003Ctitle>Exit fullscreen mode\u003C/title>\n    \u003Cpath d=\"M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z\">\u003C/path>\n\u003C/svg>\n\n\u003C/div>\n\u003C/div>\n\u003C/div>\n\n\n\n\u003Cp>Your server receives and processes the form submission.\u003C/p>\n\n\u003Cp>Using onsubmit.dev (form backend), the hosted form endpoint takes the place of that server:\u003Cbr>\n\u003C/p>\n\n\u003Cdiv class=\"highlight js-code-highlight\">\n\u003Cpre class=\"highlight plaintext\">\u003Ccode>Vue form\n   ↓\nHosted form endpoint\n   ↓\nEmail / Notion\n\u003C/code>\u003C/pre>\n\u003Cdiv class=\"highlight__panel js-actions-panel\">\n\u003Cdiv class=\"highlight__panel-action js-fullscreen-code-action\">\n    \u003Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"20px\" height=\"20px\" viewbox=\"0 0 24 24\" class=\"highlight-action crayons-icon highlight-action--fullscreen-on\">\u003Ctitle>Enter fullscreen mode\u003C/title>\n    \u003Cpath d=\"M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z\">\u003C/path>\n\u003C/svg>\n\n    \u003Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"20px\" height=\"20px\" viewbox=\"0 0 24 24\" class=\"highlight-action crayons-icon highlight-action--fullscreen-off\">\u003Ctitle>Exit fullscreen mode\u003C/title>\n    \u003Cpath d=\"M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z\">\u003C/path>\n\u003C/svg>\n\n\u003C/div>\n\u003C/div>\n\u003C/div>\n\n\n\n\u003Cp>You still have a backend involved in the overall system—you simply don't have to build, deploy, and maintain it yourself. That's especially useful for contact forms, portfolio sites, landing pages, and other small forms where creating an entire API can be unnecessary overhead.\u003C/p>\n\n\u003Ch2>\n  \u003Ca name=\"1-create-your-form-endpoint\" href=\"#1-create-your-form-endpoint\">\n  \u003C/a>\n  1. Create your form endpoint\n\u003C/h2>\n\n\u003Cp>Create a form in the service dashboard and configure where submissions should be delivered.\u003C/p>\n\n\u003Cp>For example, you can route new contact requests to your email inbox or connect a Notion destination.\u003C/p>\n\n\u003Cp>You'll receive the configuration needed to connect your Vue application to the form backend.\u003C/p>\n\n\u003Ch2>\n  \u003Ca name=\"2-add-the-vue-integration\" href=\"#2-add-the-vue-integration\">\n  \u003C/a>\n  2. Add the Vue integration\n\u003C/h2>\n\n\u003Cp>For Vue 3, use the \u003Ccode>vue-onsubmit\u003C/code> package provided by the service.\u003C/p>\n\n\u003Cp>Install the package in your project according to the current integration documentation. Using the official package means you don't need to write your own \u003Ccode>fetch()\u003C/code> request or manually manage the submission lifecycle.\u003C/p>\n\n\u003Ch2>\n  \u003Ca name=\"3-build-the-contact-form\" href=\"#3-build-the-contact-form\">\n  \u003C/a>\n  3. Build the contact form\n\u003C/h2>\n\n\u003Cp>Here's the shape of a Composition API component:\u003Cbr>\n\u003C/p>\n\n\u003Cdiv class=\"highlight js-code-highlight\">\n\u003Cpre class=\"highlight vue\">\u003Ccode>\u003Cspan class=\"nt\">&lt;\u003C/span>\u003Cspan class=\"k\">script\u003C/span> \u003Cspan class=\"na\">setup\u003C/span>\u003Cspan class=\"nt\">&gt;\u003C/span>\n\u003Cspan class=\"k\">import\u003C/span> \u003Cspan class=\"p\">{\u003C/span> \u003Cspan class=\"nx\">ref\u003C/span> \u003Cspan class=\"p\">}\u003C/span> \u003Cspan class=\"k\">from\u003C/span> \u003Cspan class=\"dl\">'\u003C/span>\u003Cspan class=\"s1\">vue\u003C/span>\u003Cspan class=\"dl\">'\u003C/span>\n\n\u003Cspan class=\"kd\">const\u003C/span> \u003Cspan class=\"nx\">name\u003C/span> \u003Cspan class=\"o\">=\u003C/span> \u003Cspan class=\"nf\">ref\u003C/span>\u003Cspan class=\"p\">(\u003C/span>\u003Cspan class=\"dl\">''\u003C/span>\u003Cspan class=\"p\">)\u003C/span>\n\u003Cspan class=\"kd\">const\u003C/span> \u003Cspan class=\"nx\">email\u003C/span> \u003Cspan class=\"o\">=\u003C/span> \u003Cspan class=\"nf\">ref\u003C/span>\u003Cspan class=\"p\">(\u003C/span>\u003Cspan class=\"dl\">''\u003C/span>\u003Cspan class=\"p\">)\u003C/span>\n\u003Cspan class=\"kd\">const\u003C/span> \u003Cspan class=\"nx\">message\u003C/span> \u003Cspan class=\"o\">=\u003C/span> \u003Cspan class=\"nf\">ref\u003C/span>\u003Cspan class=\"p\">(\u003C/span>\u003Cspan class=\"dl\">''\u003C/span>\u003Cspan class=\"p\">)\u003C/span>\n\n\u003Cspan class=\"k\">async\u003C/span> \u003Cspan class=\"kd\">function\u003C/span> \u003Cspan class=\"nf\">submitContactForm\u003C/span>\u003Cspan class=\"p\">()\u003C/span> \u003Cspan class=\"p\">{\u003C/span>\n  \u003Cspan class=\"c1\">// Submit with vue-onsubmit using the endpoint/configuration\u003C/span>\n  \u003Cspan class=\"c1\">// generated for your form.\u003C/span>\n\u003Cspan class=\"p\">}\u003C/span>\n\u003Cspan class=\"nt\">&lt;/\u003C/span>\u003Cspan class=\"k\">script\u003C/span>\u003Cspan class=\"nt\">&gt;\u003C/span>\n\n\u003Cspan class=\"nt\">&lt;\u003C/span>\u003Cspan class=\"k\">template\u003C/span>\u003Cspan class=\"nt\">&gt;\u003C/span>\n  \u003Cspan class=\"nt\">&lt;form\u003C/span> \u003Cspan class=\"err\">@\u003C/span>\u003Cspan class=\"na\">submit.prevent=\u003C/span>\u003Cspan class=\"s\">\"submitContactForm\"\u003C/span>\u003Cspan class=\"nt\">&gt;\u003C/span>\n    \u003Cspan class=\"nt\">&lt;label&gt;\u003C/span>\n      Name\n      \u003Cspan class=\"nt\">&lt;input\u003C/span> \u003Cspan class=\"na\">v-model=\u003C/span>\u003Cspan class=\"s\">\"name\"\u003C/span> \u003Cspan class=\"na\">name=\u003C/span>\u003Cspan class=\"s\">\"name\"\u003C/span> \u003Cspan class=\"na\">required\u003C/span> \u003Cspan class=\"nt\">/&gt;\u003C/span>\n    \u003Cspan class=\"nt\">&lt;/label&gt;\u003C/span>\n\n    \u003Cspan class=\"nt\">&lt;label&gt;\u003C/span>\n      Email\n      \u003Cspan class=\"nt\">&lt;input\u003C/span>\n        \u003Cspan class=\"na\">v-model=\u003C/span>\u003Cspan class=\"s\">\"email\"\u003C/span>\n        \u003Cspan class=\"na\">name=\u003C/span>\u003Cspan class=\"s\">\"email\"\u003C/span>\n        \u003Cspan class=\"na\">type=\u003C/span>\u003Cspan class=\"s\">\"email\"\u003C/span>\n        \u003Cspan class=\"na\">required\u003C/span>\n      \u003Cspan class=\"nt\">/&gt;\u003C/span>\n    \u003Cspan class=\"nt\">&lt;/label&gt;\u003C/span>\n\n    \u003Cspan class=\"nt\">&lt;label&gt;\u003C/span>\n      Message\n      \u003Cspan class=\"nt\">&lt;textarea\u003C/span>\n        \u003Cspan class=\"na\">v-model=\u003C/span>\u003Cspan class=\"s\">\"message\"\u003C/span>\n        \u003Cspan class=\"na\">name=\u003C/span>\u003Cspan class=\"s\">\"message\"\u003C/span>\n        \u003Cspan class=\"na\">required\u003C/span>\n      \u003Cspan class=\"nt\">&gt;&lt;/textarea&gt;\u003C/span>\n    \u003Cspan class=\"nt\">&lt;/label&gt;\u003C/span>\n\n    \u003Cspan class=\"nt\">&lt;button\u003C/span> \u003Cspan class=\"na\">type=\u003C/span>\u003Cspan class=\"s\">\"submit\"\u003C/span>\u003Cspan class=\"nt\">&gt;\u003C/span>\n      Send message\n    \u003Cspan class=\"nt\">&lt;/button&gt;\u003C/span>\n  \u003Cspan class=\"nt\">&lt;/form&gt;\u003C/span>\n\u003Cspan class=\"nt\">&lt;/\u003C/span>\u003Cspan class=\"k\">template\u003C/span>\u003Cspan class=\"nt\">&gt;\u003C/span>\n\u003C/code>\u003C/pre>\n\u003Cdiv class=\"highlight__panel js-actions-panel\">\n\u003Cdiv class=\"highlight__panel-action js-fullscreen-code-action\">\n    \u003Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"20px\" height=\"20px\" viewbox=\"0 0 24 24\" class=\"highlight-action crayons-icon highlight-action--fullscreen-on\">\u003Ctitle>Enter fullscreen mode\u003C/title>\n    \u003Cpath d=\"M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z\">\u003C/path>\n\u003C/svg>\n\n    \u003Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"20px\" height=\"20px\" viewbox=\"0 0 24 24\" class=\"highlight-action crayons-icon highlight-action--fullscreen-off\">\u003Ctitle>Exit fullscreen mode\u003C/title>\n    \u003Cpath d=\"M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z\">\u003C/path>\n\u003C/svg>\n\n\u003C/div>\n\u003C/div>\n\u003C/div>\n\n\n\n\u003Cp>One important note about that example: the submission function is intentionally left as a placeholder rather than inventing a \u003Ccode>vue-onsubmit\u003C/code> API. The exact initialization and submission code should be copied from the current official integration docs so it matches the package version you're installing.\u003C/p>\n\n\u003Cp>Once you've inserted that documented \u003Ccode>vue-onsubmit\u003C/code> call, the three refs—\u003Ccode>name\u003C/code>, \u003Ccode>email\u003C/code>, and \u003Ccode>message\u003C/code>—can be sent as your form payload.\u003C/p>\n\n\u003Ch2>\n  \u003Ca name=\"4-understand-what-vue-is-doing\" href=\"#4-understand-what-vue-is-doing\">\n  \u003C/a>\n  4. Understand what Vue is doing\n\u003C/h2>\n\n\u003Cp>The Composition API part is deliberately small.\u003C/p>\n\n\u003Cp>\u003Ccode>ref()\u003C/code> creates reactive values for each field, while \u003Ccode>v-model\u003C/code> keeps those values synchronized with the corresponding inputs:\u003Cbr>\n\u003C/p>\n\n\u003Cdiv class=\"highlight js-code-highlight\">\n\u003Cpre class=\"highlight javascript\">\u003Ccode>\u003Cspan class=\"kd\">const\u003C/span> \u003Cspan class=\"nx\">email\u003C/span> \u003Cspan class=\"o\">=\u003C/span> \u003Cspan class=\"nf\">ref\u003C/span>\u003Cspan class=\"p\">(\u003C/span>\u003Cspan class=\"dl\">''\u003C/span>\u003Cspan class=\"p\">)\u003C/span>\n\u003C/code>\u003C/pre>\n\u003Cdiv class=\"highlight__panel js-actions-panel\">\n\u003Cdiv class=\"highlight__panel-action js-fullscreen-code-action\">\n    \u003Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"20px\" height=\"20px\" viewbox=\"0 0 24 24\" class=\"highlight-action crayons-icon highlight-action--fullscreen-on\">\u003Ctitle>Enter fullscreen mode\u003C/title>\n    \u003Cpath d=\"M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z\">\u003C/path>\n\u003C/svg>\n\n    \u003Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"20px\" height=\"20px\" viewbox=\"0 0 24 24\" class=\"highlight-action crayons-icon highlight-action--fullscreen-off\">\u003Ctitle>Exit fullscreen mode\u003C/title>\n    \u003Cpath d=\"M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z\">\u003C/path>\n\u003C/svg>\n\n\u003C/div>\n\u003C/div>\n\u003C/div>\n\n\n\n\n\n\u003Cdiv class=\"highlight js-code-highlight\">\n\u003Cpre class=\"highlight html\">\u003Ccode>\u003Cspan class=\"nt\">&lt;input\u003C/span> \u003Cspan class=\"na\">v-model=\u003C/span>\u003Cspan class=\"s\">\"email\"\u003C/span> \u003Cspan class=\"na\">type=\u003C/span>\u003Cspan class=\"s\">\"email\"\u003C/span> \u003Cspan class=\"nt\">/&gt;\u003C/span>\n\u003C/code>\u003C/pre>\n\u003Cdiv class=\"highlight__panel js-actions-panel\">\n\u003Cdiv class=\"highlight__panel-action js-fullscreen-code-action\">\n    \u003Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"20px\" height=\"20px\" viewbox=\"0 0 24 24\" class=\"highlight-action crayons-icon highlight-action--fullscreen-on\">\u003Ctitle>Enter fullscreen mode\u003C/title>\n    \u003Cpath d=\"M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z\">\u003C/path>\n\u003C/svg>\n\n    \u003Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"20px\" height=\"20px\" viewbox=\"0 0 24 24\" class=\"highlight-action crayons-icon highlight-action--fullscreen-off\">\u003Ctitle>Exit fullscreen mode\u003C/title>\n    \u003Cpath d=\"M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z\">\u003C/path>\n\u003C/svg>\n\n\u003C/div>\n\u003C/div>\n\u003C/div>\n\n\n\n\u003Cp>The form uses Vue's \u003Ccode>@submit.prevent\u003C/code> modifier. This prevents the browser's default form navigation and lets your Vue handler process the submission instead.\u003C/p>\n\n\u003Cp>That detail is separate from onsubmit.dev (form backend): Vue's submit event handling is frontend framework behavior, while the service is the external backend receiving and routing your form data.\u003C/p>\n\n\u003Ch2>\n  \u003Ca name=\"5-handle-success-and-failure-states\" href=\"#5-handle-success-and-failure-states\">\n  \u003C/a>\n  5. Handle success and failure states\n\u003C/h2>\n\n\u003Cp>For a production form, it's worth adding a few UI states around the integration:\u003C/p>\n\n\u003Cul>\n\u003Cli>Disable the button while the request is being submitted.\u003C/li>\n\u003Cli>Show a confirmation such as \"Thanks! Your message was sent.\"\u003C/li>\n\u003Cli>Display a useful error if submission fails.\u003C/li>\n\u003Cli>Clear the fields after a successful submission.\u003C/li>\n\u003Cli>Keep client-side validation for immediate feedback.\u003C/li>\n\u003C/ul>\n\n\u003Cp>The hosted endpoint handles delivery, but the Vue component remains responsible for creating a good user experience.\u003C/p>\n\n\u003Ch2>\n  \u003Ca name=\"email-or-notion\" href=\"#email-or-notion\">\n  \u003C/a>\n  Email or Notion?\n\u003C/h2>\n\n\u003Cp>Where the data goes depends on how you configure the form rather than how you build the Vue inputs.\u003C/p>\n\n\u003Cp>For a personal portfolio, sending messages to email may be enough. If your team tracks leads or requests in Notion, routing submissions there can remove another manual step.\u003C/p>\n\n\u003Cp>That separation is useful: your Vue component collects the data, while the external form backend deals with routing it.\u003C/p>\n\n\u003Ch2>\n  \u003Ca name=\"when-does-this-approach-make-sense\" href=\"#when-does-this-approach-make-sense\">\n  \u003C/a>\n  When does this approach make sense?\n\u003C/h2>\n\n\u003Cp>Using a managed form backend is a good fit when forms aren't the core of your application's business logic.\u003C/p>\n\n\u003Cp>A contact page shouldn't necessarily require you to create a Node server, configure an email provider, store secrets, deploy an API, and monitor all of that infrastructure.\u003C/p>\n\n\u003Cp>For applications requiring complex authorization, transactional workflows, extensive server-side business rules, or tight integration with an existing database, your own backend may still be the better choice.\u003C/p>\n\n\u003Cp>For a straightforward Vue contact form, though, the architecture can stay refreshingly small:\u003Cbr>\n\u003C/p>\n\n\u003Cdiv class=\"highlight js-code-highlight\">\n\u003Cpre class=\"highlight plaintext\">\u003Ccode>Vue 3 + Composition API\n          ↓\n     vue-onsubmit\n          ↓\n Hosted form backend\n          ↓\n    Email / Notion\n\u003C/code>\u003C/pre>\n\u003Cdiv class=\"highlight__panel js-actions-panel\">\n\u003Cdiv class=\"highlight__panel-action js-fullscreen-code-action\">\n    \u003Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"20px\" height=\"20px\" viewbox=\"0 0 24 24\" class=\"highlight-action crayons-icon highlight-action--fullscreen-on\">\u003Ctitle>Enter fullscreen mode\u003C/title>\n    \u003Cpath d=\"M16 3h6v6h-2V5h-4V3zM2 3h6v2H4v4H2V3zm18 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z\">\u003C/path>\n\u003C/svg>\n\n    \u003Csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"20px\" height=\"20px\" viewbox=\"0 0 24 24\" class=\"highlight-action crayons-icon highlight-action--fullscreen-off\">\u003Ctitle>Exit fullscreen mode\u003C/title>\n    \u003Cpath d=\"M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z\">\u003C/path>\n\u003C/svg>\n\n\u003C/div>\n\u003C/div>\n\u003C/div>\n\n\n\n\u003Cp>The frontend remains a normal Vue application, while form delivery becomes managed infrastructure rather than another backend project you have to maintain.\u003C/p>\n\n","# Vue 3 Contact Forms Without Writing a Single Line of Backend Code\n\nBuilding a contact form in Vue is easy until you get to the part where the submission has to go somewhere. Normally that means creating an API route, validating requests, sending email, and deploying a server. With [onsubmit.dev (form backend)](https://onsubmit.dev), you can instead connect a Vue 3 form to a hosted endpoint and route submissions to destinations such as email or Notion.\n\nIn this tutorial, we'll build a contact form with Vue 3's Composition API and the `vue-onsubmit` integration.\n\n## Why can we skip the backend?\n\nA browser shouldn't send email directly or contain private credentials for third-party APIs such as Notion.\n\nA traditional setup therefore looks roughly like this:\n\n```text\nVue form\n   ↓\nYour API/server\n   ↓\nEmail / Notion\n```\n\nYour server receives and processes the form submission.\n\nUsing onsubmit.dev (form backend), the hosted form endpoint takes the place of that server:\n\n```text\nVue form\n   ↓\nHosted form endpoint\n   ↓\nEmail / Notion\n```\n\nYou still have a backend involved in the overall system—you simply don't have to build, deploy, and maintain it yourself. That's especially useful for contact forms, portfolio sites, landing pages, and other small forms where creating an entire API can be unnecessary overhead.\n\n## 1. Create your form endpoint\n\nCreate a form in the service dashboard and configure where submissions should be delivered.\n\nFor example, you can route new contact requests to your email inbox or connect a Notion destination.\n\nYou'll receive the configuration needed to connect your Vue application to the form backend.\n\n## 2. Add the Vue integration\n\nFor Vue 3, use the `vue-onsubmit` package provided by the service.\n\nInstall the package in your project according to the current integration documentation. Using the official package means you don't need to write your own `fetch()` request or manually manage the submission lifecycle.\n\n## 3. Build the contact form\n\nHere's the shape of a Composition API component:\n\n```vue\n\u003Cscript setup>\nimport { ref } from 'vue'\n\nconst name = ref('')\nconst email = ref('')\nconst message = ref('')\n\nasync function submitContactForm() {\n  // Submit with vue-onsubmit using the endpoint/configuration\n  // generated for your form.\n}\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Cform @submit.prevent=\"submitContactForm\">\n    \u003Clabel>\n      Name\n      \u003Cinput v-model=\"name\" name=\"name\" required />\n    \u003C/label>\n\n    \u003Clabel>\n      Email\n      \u003Cinput\n        v-model=\"email\"\n        name=\"email\"\n        type=\"email\"\n        required\n      />\n    \u003C/label>\n\n    \u003Clabel>\n      Message\n      \u003Ctextarea\n        v-model=\"message\"\n        name=\"message\"\n        required\n      >\u003C/textarea>\n    \u003C/label>\n\n    \u003Cbutton type=\"submit\">\n      Send message\n    \u003C/button>\n  \u003C/form>\n\u003C/template>\n```\n\nOne important note about that example: the submission function is intentionally left as a placeholder rather than inventing a `vue-onsubmit` API. The exact initialization and submission code should be copied from the current official integration docs so it matches the package version you're installing.\n\nOnce you've inserted that documented `vue-onsubmit` call, the three refs—`name`, `email`, and `message`—can be sent as your form payload.\n\n## 4. Understand what Vue is doing\n\nThe Composition API part is deliberately small.\n\n`ref()` creates reactive values for each field, while `v-model` keeps those values synchronized with the corresponding inputs:\n\n```js\nconst email = ref('')\n```\n\n```html\n\u003Cinput v-model=\"email\" type=\"email\" />\n```\n\nThe form uses Vue's `@submit.prevent` modifier. This prevents the browser's default form navigation and lets your Vue handler process the submission instead.\n\nThat detail is separate from onsubmit.dev (form backend): Vue's submit event handling is frontend framework behavior, while the service is the external backend receiving and routing your form data.\n\n## 5. Handle success and failure states\n\nFor a production form, it's worth adding a few UI states around the integration:\n\n- Disable the button while the request is being submitted.\n- Show a confirmation such as \"Thanks! Your message was sent.\"\n- Display a useful error if submission fails.\n- Clear the fields after a successful submission.\n- Keep client-side validation for immediate feedback.\n\nThe hosted endpoint handles delivery, but the Vue component remains responsible for creating a good user experience.\n\n## Email or Notion?\n\nWhere the data goes depends on how you configure the form rather than how you build the Vue inputs.\n\nFor a personal portfolio, sending messages to email may be enough. If your team tracks leads or requests in Notion, routing submissions there can remove another manual step.\n\nThat separation is useful: your Vue component collects the data, while the external form backend deals with routing it.\n\n## When does this approach make sense?\n\nUsing a managed form backend is a good fit when forms aren't the core of your application's business logic.\n\nA contact page shouldn't necessarily require you to create a Node server, configure an email provider, store secrets, deploy an API, and monitor all of that infrastructure.\n\nFor applications requiring complex authorization, transactional workflows, extensive server-side business rules, or tight integration with an existing database, your own backend may still be the better choice.\n\nFor a straightforward Vue contact form, though, the architecture can stay refreshingly small:\n\n```text\nVue 3 + Composition API\n          ↓\n     vue-onsubmit\n          ↓\n Hosted form backend\n          ↓\n    Email / Notion\n```\n\nThe frontend remains a normal Vue application, while form delivery becomes managed infrastructure rather than another backend project you have to maintain.",{"name":32,"username":33,"twitter_username":33,"github_username":13,"user_id":34,"website_url":35,"profile_image":36,"profile_image_90":37},"Yevhen Kozachenko 🇺🇦🇩🇪","ekwoster",3415300,"https://ekwoster.dev","https://media2.dev.to/dynamic/image/width=640,height=640,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3415300%2F145683af-9716-4263-812e-25a3962d2552.jpg","https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3415300%2F145683af-9716-4263-812e-25a3962d2552.jpg",1788519848668]