How to Create Lead Generation Forms with HTML and Backend Integration

CODEJAVASCRIPT19 min

Learning Content

Watch the video and follow along with the written guide

Build a Form Project

You'll want to create a form for your online business for lead generation or email marketing. In today's exercise we'll be building two forms with HTML and connecting it to the backend.

What You'll Need

  • Visual Studio Code
  • Github account
  • Supabase account (Free)
  • Mailchimp account (Free)
  • Render account (Free)

The project is designed to help you understand how to set up a front end and link it to a backend without overcomplicating it. This project will teach you the building blocks of forms and how you can use one for your business.

FORM #1 - Lead Generation

  • Github - Stores your project code
  • Render - Hosts your Express code
  • Supabase - Hosts your database

FORM #2 - Email Marketing

  • Github - Stores your project code
  • Render - Hosts your Express code
  • Mailchimp - Hosts your database

Before getting started it is important to understand an overview of how everything works together.

  1. On your computer you will create a folder for all your code files. This will later be uploaded to Github.
  2. You will then set up Render which will link to your Github account to pull the backend code (in this case Express). This backend code will communicate with the database.
  3. You will finally set up a database (Supabase for the lead generation form, Mailchimp for the email marketing form).

That's it. The form will only have one input field (email) for simplicity. This project is a starting point to then expand upon it however you'd like. When first learning to code I often found the connection of front end to back end confusing. Projects like these should help you connect the dots where you can then touch it up with CSS or apply to your use case.

Finally I personally like to have flexibility. I have selected these providers on factors such as popularity and affordability. There is always a company offering a new tool or service on how to build on the web, but the good news is that you can take these skills and apply them elsewhere. Want to use a different email marketing platform? No problem, just export your database as a CSV with mailchimp. Want to use a different database provider? No problem, just take the concepts and apply them elsewhere. Flexibility is also important, because when you are at the early stages of your idea you want to:

  1. Iterate quickly and not get stuck in the infinite loop and not make a decision
  2. Be able to adapt quickly as your idea changes over time.

It is inevitable that your business will change. Every founder I have spoken to and/or worked for has modified their idea over time. You want to be adaptable by not picking legacy tools that make it difficult to change, but also reasonable to select the option that is the best fit with the information you have at the time. These tools should make it easy to leave as well. There is no need to try and know everything. Just know enough to make that delta for your business.

FORM #1 - Lead Generation Instructions

  1. Open a new folder and name it lead-form
  2. Create an index.html file and build the form

In the code you will see HTML boilerplate, an H1 heading element, and a form element. For the sake of brevity I will assume you are familiar with the boilerplate and heading elements and we will only review the form element. A quick Google search will help provide additional insight if anything is unclear.

I found that I struggled remembering the elements of forms constantly. To keep it easy we will be only using some of the fundamental building blocks of forms however do keep in mind that there are four categories to consider when building forms:

  1. 📥 Form Controls -- Submit data
  2. 📐 Layout & Structure -- Organize and style the form
  3. ⚙️ Interactivity & Behavior -- Make the form dynamic or user-responsive
  4. 🧾Media & Extra Content -- Provide guidance, visuals, or enrich the experience

In this project we will only be dealing with Form Controls.

A form can be built with the following elements:

  • The form element
  • The button element
  • The input element
  • The label element

Lets bundle them into two categories so that you will remember them.

First, the form element is easy to remember, you are building a form after all. Unlike a div it has semantic meaning and includes attributes that help a computer know where to send the data inserted into the form. The button element initiates the process of sending the data.

Second, you need to collect data so inside the form element you have input elements. Input elements are the boxes you type in your data you want to send to the database. For accessibility reasons you also want the label element. Visually the label element appears next to the box where you type in your data. Labels are not required to send data to a server. If you left it out the form would still work. Labels are required for every user's sanity when completing a form on your website. So add those labels.

Great now you understand the four main elements of forms: (form & button) (input & label).

Within elements you have attributes. Lets go through the main ones used in this project.

Attributes for the Form Element: I mentioned that Form elements are useful for telling a computer where to send data. Form elements include an action attribute and a method attribute. The value of an action attribute is endpoint url for a server that tells a computer where data is going. You get this url from Render after creating a new project.

The method attribute tells the computer the intention of the data. There are two types GET and POST but generally you will always use POST.

  • GET: When you are doing read-only actions like searching, filtering, viewing
  • POST: When you are creating or changing something within your database

Attribute of the Submit Element: The type attribute provides semantic meaning to a computer. An element will act based on the assigned type and in some case look differently in the browser. In this case we are telling the computer that this button will trigger the sending of data inserted into the form.

You can think of type like a job. I tell you that you are a waiter, you will act like a waiter. I tell you that you are firefighter, you will act like a firefighter. Same goes for type.

Attribute of the Label Element: The for attribute is needed so that you can properly link the label to the input. Think of the label and input elements as lego pieces and you need to piece them together. The for attribute helps you do that.

Attributes of the Input Element: To complete this connection with the for attribute you will use the id attribute. The id is an identifier and you will notice that the value "email" of the for attribute is the same as the id. We are using a type of email for this element, so the element will expect email data in the input box. The next two attributes are name and placeholder. Name serves as the key to the value on the backend (key-value pairs) and placeholder serves as some subtle grayed-out text that appears within the input box before a user clicks in the box to begin typing. The final attribute is required. Whenever you add this attribute you are telling the computer that a user must input data into the field before the form can be submitted to the server. The placeholder attribute and required attribute are optional.

  1. Sign up for a Supabase account and build the database

To build the database you need to create a new project. Then in the left sidebar click Table Editor and create a new table. Give the table a name lead-form and then add a column that you want to drag just below the id column - column_name should be email and type should be text. Save your changes.

Click the gear icon called Project Settings and then Data API in the sidebar. Copy the Project URL and the Public API Key. You'll need both for the next bit.

  1. Create an server.js file and use Express to send the form data to the database.

Lets breakdown this code.

First you have setup and imports. This is where we gather up all our tools that will be needed for our server to send data to the database. Express, Supabase, dotenv, & CORS.

Similar to HTML, express also has some boilerplate code that you always need. Here we are building the express server. To communicate with our Supabase database you need the createClient function.

Your express server needs to know where your database is and createClient helps with providing those details and completing the connection. createClient sets up a connection between the express server and database with the sensitive supabase information you just grabbed.

You just grabbed sensitive data from Supabase. We want to store that securely in what is known as a .env file. The .env file is only needed for testing as will add environment variables directly within Render; however you can think of .env as a simulation of that step. Since our server isn't live we need to put our sensitive details somewhere. We will then use dotenv to allow us to reference those variables in .env or within Render once it is live. We reference environment variables by using process.env. before the variable name.

While you could hardcode the sensitive details within the code during testing this wouldn't be recommended as you would need to remember to update the code with dotenv before going live. Since you need to do this step, might as well use a .env file. Later you will create a .gitignore file to exclude .env from your repository.

CORS helps websites share information with each other safely.

Middleware

Think of Express as a tool that builds on top of the basic HTTP server (Node.js's http module) to make it easier for you to handle common tasks when building web applications.

While the HTTP server is responsible for receiving requests and sending responses, Express adds extra functionality

When you submit a form using the regular <form> HTML tag with a POST method, the browser sends the data as URL-encoded format by default.

Initialize Supabase

The Supabase client is a JavaScript object that provides methods for interacting with your Supabase project. It's a wrapper built on top of HTTP, much like how Express is built on top of Node's native http module. The client simplifies communication with your database by automatically making HTTP requests to the Supabase backend. Essentially, the Supabase client abstracts away the complexities of direct HTTP calls, making it easier for developers to interact with their database.

Navigate to your Table-Editor and select lead-form. In the top toolbar select Auth policy. We want to add a new RLS (Row-Level Security) policy to allow anyone to update the database. Click Create Policy and then update the field to get the following:

The field should be Policy Name: Enable insert for all users

Table on clause: public.leads

Policy Behavior: Permissive Policy Command: INSERT

Target Roles to clause: Defaults to all (public) roles if none selected

The Supabase RLS policy is essentially doing the same thing as this SQL command which you can also add in the SQL editor:

Warning: All anonymous users can insert data into the database which exposes you to spam. Consider enabling Honeypot field, Rate limiting, or reCAPTCHA with Google.

This won't be necessary for the email-marketing form linked to Mailchimp.

API Route

We're creating an API route that allows us to handle form data and update our database. This route is set up using the POST method, which is commonly used to send data from the client to the server. The specific path we're using is /api/submit. The /api part is a common convention that signals this endpoint is for application logic rather than a webpage. The /submit part tells us that this route is meant for submitting something --- in this case, form data like an email address.

The function attached to this route is asynchronous because we're performing a database operation, which takes time. The POST method takes two arguments: the route path (/api/submit) and a callback function that handles the logic when a request comes in. This function receives two objects: req (the request) and res (the response). These represent the communication between the client (like a browser) and the server. When a user fills out a form and clicks submit, a request is sent to the server. Inside the function, we pull the email value from the request body using req.body.email.

To save this email in our Supabase database, we use Supabase's special methods. Specifically, we use .insert([{ email }]), which sends the data to a table (in this case, a table called leads). That line also returns an object that contains two parts: data and error. We use object destructuring to grab those values directly. The data property will be an array of the newly inserted row(s) if everything works correctly, and error will be null. If something goes wrong, data will be null and error will be an object that contains a message and other details about the failure.

We always check for errors before proceeding. If there's an error, we log it and return a response to the client letting them know something went wrong. If everything goes smoothly, we send a success message using res.send(). Every API route needs to complete this conversation by sending a response back to the client --- whether it's success or failure.

You might also notice the syntax [{ email }], which is a shorthand way of writing { email: email }. This works because the key and value have the same name. It makes the code shorter and easier to read, especially when working with form data.

In summary, Supabase responds with a consistent structure: a data field containing the inserted rows (as an array of objects), and an error field that's either null or an object with details about what went wrong. These two properties together are included in the full response object from Supabase.

Start Server

app.listen is a method provided by the Express object (specifically the express() instance) that starts a server and makes it listen for incoming HTTP requests on a specified port. This line is responsible for starting the server and making it listen for incoming requests. The method takes two arguments: the port and a callback function. The port is typically set using the environment variable process.env.PORT, which can be configured externally (for example, in a hosting environment). If process.env.PORT isn't set, it defaults to 3000 (in our code), which is common port for local development.

The callback function is an optional argument that is a function passed as an argument to another function and is executed after that function completes its task. In this case, the callback logs a message to the console stating that the server is active. While the server would technically still work without this callback, including it is a common practice, as it provides visual confirmation that the server is up and running. Without it, you wouldn't know the server was active unless you checked for errors or tried making requests. Therefore, it's best to leave the callback in for clarity and confirmation.

  1. Create a .env to store environment variables

    In you lead-form folder create a file called .env. Here we want to add our sensitive Supabase information. The format is a key in all caps, an equal sign, and the value. Do not wrap the value in quotes. Just paste it directly next to the equal sign. dotenv will be able to read this file which allows us to keep sensitive data out of our code. This sensitive data will later be directly added to render since we will be excluded .env from our repository in the .gitignore file which we are creating next.

  2. Create a .gitignore file in the lead-form folder to leave node modules and the .env file out. If you don't do this you risk sharing sensitive data and can cause issues with building on render

    This prevents sensitive data from getting out and also reducing the file size of our project. Node modules are tools needed for our specific project to run but we don't need to include them in the repository. They will be installed when needed by the server.

  3. Sign up for Github

    We now want to upload our folder to Github as a repository. You'll want to create a new repository and then push your changes to Github.

  4. Sign up for Render - Cloud Application Platform

After signing up for Render you want to create a new Project, specifically a new web service which you can create by selecting the + Add New in the top right corner of the dashboard screen. Name the project lead-form, ensure the build command is npm install, start command is node server.js. Select the free plan and then copy and paste your environment variables from your .env file. Then click Deploy Web Service.

After deploying copy the url given for your project add to the tail /submit and then within your lead-form folder on your computer replace the form action method with that url.

FORM #2 - Email Marketing Instructions

We will now want to build a new form to link to Mailchimp for free. You'll start to notice similarities with the first form we setup.

  1. Open a new folder and name it email-marketing

  2. Create an index.html file and build the form

    You'll notice the form is exactly the same except for the title tag so you can just copy and paste from your previous project.

  3. Sign up for a Mailchimp account and build the database

    After signing up for Mailchimp you need to get three items. Your API Key, Audience ID, and Data Center. First click on your photo in the top right corner and select Profile in the dropdown. Under Extras click API keys. Then click Create a Key and call it Email Marketing Form. Generate the key and save it for your records. Next go to Audience in the left sidebar and select settings from there you will see your Audience ID. Save that id for your records. As for the data center this will be what you see at the end of your API Key starting with "us". Save that for your records.

  4. Create a .env to store environment variables (the three items you pulled from Mailchimp. You are creating a .env file like before but this time have different variables.

  5. Create a .gitignore file to leave node modules and the .env file out. If you don't do this you risk sharing sensitive data and can cause issues with building on render. This is the same as the previous lead-form we built.

  6. Create an server.js file and use Express to send the form data to the database.

This code goes into an API folder because it separates your backend logic (API routes) from your frontend routes (pages the user sees).

Example:

  • /about → serves an HTML page
  • /api/submit → handles the form data and updates Mailchimp

Mailchimp expects a JSON payload. You have to convert your JavaScript object into a JSON string

fetch() is a built-in JavaScript function used to make HTTP requests (like GET or POST) from the browser or in Node (with some setup). You use it to talk to APIs. This situation we are sending form data to Mailchimp.

  1. Create a new project in Render - Cloud Application Platform

Everything is the same as the lead-form but this time call it email-newsletter

That's it! For additional enhancement I recommend adding additional fields to the form, styling the form with CSS, and adding security measures to prevent spam. You can take this foundational project an apply it to your use case.

Congrats on completing the assignment!

Code Sandbox

Write, run, and test your code in real-time

Ready to practice?
More Videos
© 2025 MATTHEW SEIWERT