DEV Community

Cover image for Web Fundamentals: HTML Forms
Hasan Ali
Hasan Ali

Posted on

Web Fundamentals: HTML Forms

Contents

  1. What are forms?
  2. How do they work?
  3. How do we build it?
  4. Summary

1. What are forms?

Forms are one of the fundamental tools through which we interact with the web everyday [1]. It enables us to sign into apps, signup for newsletters, purchase things online and post comments to list a few. They're a key mechanism through which we send information from our browser to the server.

In this tutorial, we're going to start with the simplest incarnation of forms and start to gain some appreciation for just how much functionality is built into the web platform. We'll go over how they work and how to build it. To follow along, you'll need a text editor, and a web server that can serve HTML files and handle requests. For my web server, I'm going to use Deno [2] and a web framework called Oak [3] to handle requests. On the frontend, it'll be plain HTML.

Prerequisites:

  • Basic understanding of how servers work [MDN]
  • HTTP verbs [MDN]
  • An installation of Deno [Website]

2. How do they work?

Before we dive into forms, let's start with what happens when you visit a website:

  1. You request for a website through the address bar or a link
  2. The server receives your request
  3. The server responds to your request by sending HTML as text
  4. Your browser renders that HTML

When you visit a site, the request that you make to the server is a GET request. The server needs to be configured to handle requests based on the path of the website being accessed and also what type of request is being made (i.e. GET, POST or any of the other HTTP verbs [4]). For example, when you access the site https://hasanhaja.com/picks then you make a GET request to the path /picks. Where as if you go to https://hasanhaja.com then you make a GET request to the path / .

With that context, let's follow an example where the HTML that is returned contains a form.

A diagram showing the client and server relationship when the client requests for a web page. There are two boxes containing the text "Client" and "Server" with two arrows going between them. Above the arrow going from "Client" to "Server" is the text "GET / req", and below the arrow going from "Server" to "Client" is the text "res" and "HTML". To the right of the "Client" box is an HTML snippet with a part of a form shown.

The form is denoted by the HTML tag <form> and nested within it are fields for the data we want to collect along with a submit button. The default behavior of forms is the construct a search query with the form data and make another GET request to the same path.

A color-coded diagram showing the client and server relationship when the client sends form data to the server. The HTML on the right hand side of the client box has been color-coded with orange to represent what will become the form data and blue to represent the submit action. Consequently, the arrow and the GET request label is blue with only the form data "name=Hasan" colored orange.

The server can be configured to identify the search query parameters so that it can respond as instructed.

The same diagram but with a response arrow from the server to the client with the text "res" below it and a box containing the word "HTML" to denote that it is an HTML response.

There's also another way forms can be configured on the frontend. If we'd like the form to submit data by making a POST request to the same path with the form data in the body of the request, we can add the method attribute to the HTML like this <form method="post">. The server can be configured very similarly to handle this request.

For the form's POST version of the same client-server relationship diagram, the text above the arrow going from "Client" to "Server" has the text "POST / req", and above that is an orange box that denotes the form data sent with the POST request. To the right of the "Client" box is an HTML snippet with a part of a form shown with the "method" attribute set to "post".

Similar response diagram with a response arrow from the server to the client with the text "res" below it and a box containing the word "HTML" to denote that it is an HTML response.

In the above example, we only see the server return HTML as a response to the request, but in actuality it can be configured to perform any arbitrary action with the data. If this were a form to subscribe to a newsletter, then the server will perform the logic to add them to the email list and then send an HTML response to inform the user that the request was handled successfully.

If the form field were a search filter in an e-commerce website, then the form data can be used to query the database for all the products related to the search and then the response can be constructed with these results. The applications for forms are endless, and that's precisely why they're everywhere.

3. How do we build it?

Okay, let's build a form and start tinkering!

To keep things as simple as possible, our form will contain the minimum number of fields required for it to qualify as a form. Here's a form with one text field [5] called "name":

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <title>Form</title>
    </head>
    <body>
        <h1>Our form</h1>
    <form>
      <h2>Your details</h2>
      <label for="name">Name</label>
      <input id="name" name="name" type="text">
      <button type="submit">Submit</button>
    </form>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

This is what the HTTP request looks like when you click submit:

GET /?name=Hasan HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/114.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Referer: http://localhost:8000/
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Enter fullscreen mode Exit fullscreen mode

The HTML form constructed this entire HTTP request for us, and we only need to consume this on the server and send a response back. Let's look at the server code that can handle this form submission:

import { Application, Router } from "oak";

const router = new Router();

router
  .get("/", async (context) => {
      const { search } = context.request.url;

      if (!search) {
        await context.send({
          root: `${Deno.cwd()}/static`,
          index: "index.html",
        });
        return;
      }

      const params = new URLSearchParams(search);
      const name = params.get("name");
      context.response.body = `<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Form</title>
  </head>
  <body>
    <h1>Our form</h1>
    <p>Hi there, ${name}!</p>
  </body>
</html>
      `;
});

const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8000 });
Enter fullscreen mode Exit fullscreen mode

The .get handler for the path "/" handles both the request for the page and the form submission. The conditional check if (!search) is to distinguish between the form being requested or the form being submitted. When the page is requested without the search query parameters, it means the user is requesting the form, and when it's requested with the query parameters it means the form was submitted. In our implementation when we encounter a form submission, we construct HTML inline and respond with the name in the body of the HTML.

The functionality of context.send is to read the static HTML file from the server's filesystem and send it as text in the response. The Oak framework constructs an HTTP response with the appropriate headers to tell the browser what kind of content it will be rendering. You can debug the response headers in the Network tab of the browser's developer tools, and this is what it looks like for our form submission response:

HTTP/1.1 200 OK
content-type: text/html; charset=UTF-8
vary: Accept-Encoding
content-encoding: gzip
content-length: 187
date: Mon, 10 Jul 2023 22:05:42 GMT
Enter fullscreen mode Exit fullscreen mode

Aside: The content-type: text/html is why responding with an inlined HTML string by setting the context.response.body is both valid and equivalent to the context.send approach. The context is an implementation detail specific to the Oak framework, and your mileage may vary depending on how you handle HTTP requests.

This can get a little confusing on the server. It's confusing because when the form data is sent to the server, the underlying HTTP verb is still GET. That means when the server receives a GET request, it also needs to check if form data is being sent to it. Using GET with data is a perfectly valid pattern, but we can make our lives a little easier when dealing with forms to separate the logic for getting forms and posting data.

To switch this form to send the form data via POST, we can add the "method" attribute with the value "post" to the form tag.

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <title>Form</title>
    </head>
    <body>
        <h1>Our form</h1>
    <form method="post">
      <h2>Your details</h2>
      <label for="name">Name</label>
      <input id="name" name="name" type="text">
      <button type="submit">Submit</button>
    </form>
    </body>
</html>
Enter fullscreen mode Exit fullscreen mode

This seems too trivial and it is. The web platform is incredibly powerful and feature-rich. This little declaration entirely changed the communication mechanism. This is the HTTP request that gets sent to the server:

POST / HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/114.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 10
Origin: http://localhost:8000
Connection: keep-alive
Referer: http://localhost:8000/
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Enter fullscreen mode Exit fullscreen mode

And this is the payload that goes with the request:

name=Hasan
Enter fullscreen mode Exit fullscreen mode

On the server, we can handle this by adding another handler to our router:

/* --snip-- */
router
  .get("/", async (context) => {
      const { search } = context.request.url;

      if (!search) {
        await context.send({
          root: `${Deno.cwd()}/static`,
          index: "index.html",
        });
        return;
      }
            /* --snip-- */
  })
  .post("/", async (context) => {
    const formBody = await context.request.body({ type: "form" }).value;
    const name = formBody.get("name");

    context.response.body = `<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <title>Form</title>
    </head>
    <body>
        <h1>Our form</h1>
    <p>Hi there, ${name}!</p>
    </body>
</html>
    `;
  })
;

/* --snip-- */
Enter fullscreen mode Exit fullscreen mode

We'll leave the .get handler as is so it can still serve the form to us, but when we submit it our .post handler will handle that request. And that's how simple forms are!

This is our entire server code that handles form submissions through the GET and POST method:

import { Application, Router } from "oak";

const router = new Router();

router
  .get("/", async (context) => {
      const { search } = context.request.url;

      if (!search) {
        await context.send({
          root: `${Deno.cwd()}/static`,
          index: "index.html",
        });
        return;
      }

      const params = new URLSearchParams(search);
      const name = params.get("name");
      context.response.body = `<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Form</title>
  </head>
  <body>
    <h1>Our form</h1>
    <p>Hi there, ${name}!</p>
  </body>
</html>
      `;
  })
  .post("/", async (context) => {
    const formBody = await context.request.body({ type: "form" }).value;
    const name = formBody.get("name");

    context.response.body = `<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <title>Form</title>
    </head>
    <body>
        <h1>Our form</h1>
    <p>Hi there, ${name}!</p>
    </body>
</html>
    `;
  })
;

const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8000 });
Enter fullscreen mode Exit fullscreen mode

All the source code for this post can also be found on my GitHub.

Summary

Forms are simple and they are deeply supported in the web platform. They enable us to capture data from users and this provides an interactive web experience. In this post, we looked at the anatomy of HTML form and how to build them. We saw two methods of submitting form data to the server and we saw that although the difference in code is very minimal, the communication of intent is far stronger with the POST method.

HTML forms have a lot of features packed into them. There are a lot more fun and complex situations that require delving into them deeply. Examples of this can be:

  • Validating the input to ensure it's in the correct format before sending it to the server
  • Validating the data on the server and responding with an error message if the validations fail

Stay tuned for follow up posts in this web fundamentals series.

If you think of anything I've missed or just wanted to get in touch, you can reach me through a comment, via Mastodon, via Threads, via Twitter or through LinkedIn.

References

  1. Web forms — Working with user data [MDN]
  2. Deno [Website]
  3. Oak framework [Website]
  4. HTTP verbs [MDN]
  5. Form Input element [MDN]

Top comments (0)