Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Overview

This section shows a minimal example of implementing a user registration form with FastAPI
we want to focus on the aspects relating to forms, and for that reason we deliberately keep things simple[1].

In the mix, we’ll take this chance to say a word on CORS, which is a rather painful impediment in web development especially for beginners, so that you are aware of it and know how to work around that.


→ The backend

We’re going to elaborate on the simplistic backend example from python/db-single-table, remember:

users.py
from sqlmodel import SQLModel, Field

class User(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    email: str
    is_active: bool = True

reminders:


→ The <form> HTML element (frontend)

When building an HTML page that prompts the user to enter data, the most common approach is to use a <form> element, which provides a structured way to collect user input and submit it to a server.

So in our case we write a form with three input fields and a submit button like so

register.html
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<form id="registration-form" action="/users/" method="post">
  <label>
    Name:
    <input type="text" name="name" required>
  </label>

  <label>
    Email:
    <input type="email" name="email" required>
  </label>

  <label>
    Active:
    <input type="checkbox" name="is_active" checked>
  </label>

  <button type="submit">Register</button>
</form>

Implicit bindings & default encoding

Important observations:

This means that the form is implicitly bound to the API endpoint, without any explicit coupling in the code.

However, there’s a catch: by default, the browser will submit the form data as application/x-www-form-urlencoded[3], which is not JSON !

And as we actually want to send JSON, we need to intercept the submission with JavaScript.


→ Converting form data to JSON

And here’s how we can do that[4]

hijack-forms.js
document.addEventListener("DOMContentLoaded", () => {
    document.querySelectorAll('form').forEach((form) => {
        const formToJSON = form => Object.fromEntries(new FormData(form))
        form.addEventListener("submit", async (event) => {

          // the default behaviour (sending form data as urlencoded) is
          // precisely what we DON'T WANT, so we prevent it
          event.preventDefault()

          // convert the form data into a plain JavaScript object
          const json = formToJSON(form)

          // use the action= and method= attributes
          //  to determine where to send the data
          const {action, method} = form
          const response = await fetch(action, {
            method,
            headers: {
              "Content-Type": "application/json",
              "Accept": "application/json",
            },
            body: JSON.stringify(json),
          })
          if (!response.ok) {
            console.error(`Error submitting form at ${action} : `,
              response.statusText)
            return
          }
          const decoded = await response.json()
          console.log("response", decoded)
        })
      })
    })

What happens here when the form gets submitted:

  1. The browser collects form values into a FormData object

  2. We convert it into a plain JavaScript object

  3. The object is serialized to JSON

  4. The request is sent with Content-Type: application/json

This keeps the backend clean and JSON-focused.


Other types of inputs

In our html snippet, we’ve used 2 different types of inputs:

but as you can imagine, there are many other types of inputs, e.g. <input type="date"> for dates, <input type="file"> for file uploads, etc...

Make sure to pick the one that works best for you.


→ CORS breaks the naive way

When trying to run the above code a bit too naively, i.e. with

And then opening http://localhost:5173/register.html, you will notice the code won’t work

The obvious reason is this: we have writtenaction="/users/" in the form,
which means the browser will try to submit the form to http://localhost:5173/users/,
which is not where our backend is running - it listens on port 8000

Now, as an exercise, change the action attribute and replace it with http://localhost:8000/users/ - you will see that this still won’t work !

Open your web console: in this configuration we hit a rather painful reality of web development: CORS restrictions in the browser.


What is CORS ?

CORS stands for Cross-Origin Resource Sharing, and it’s a security mechanism implemented by browsers to prevent malicious websites from making unauthorized requests to other domains.

When you try to submit the form to http://localhost:8000/users/ from a page served on http://localhost:5173, the browser considers this a cross-origin request and blocks it by default.

This situation happens every time a web page tries to fetch() from another domain than the one it was served from, and it’s a common hurdle in web development.


→ A workaround: vite as a proxy

For development purposes, a common workaround is to use a proxy, which allows us to forward requests from the frontend server to the backend server, effectively bypassing CORS restrictions.

You can see this in action in the python/fastapi-forms folder, where we have a vite.config.js file which configures vite to proxy requests starting with /users to http://localhost:8000/users.

This means you can run the original code by simply running vite without the --config /dev/null option; this time vite will read the configuration and set up the proxy, allowing you to keep the form’s action="/users/" as it is, and everything will work seamlessly.


A production architecture

Once you have all these pieces working, you can then then envision a production architecture where:

and this has many advantages:


Summary


Footnotes
  1. we keep the schema minimal to focus on form handling; in practice you might want to use more specific types (e.g. User and UserCreate), and add constraints (e.g. email format validation), as we’ve seen in previous sections

  2. here of course we assume you run the backend from the db-single-table folder, on port 8000

  3. this encoding format is a legacy from the 1990s, the early days of the web, and is still the default in browsers for form submissions

  4. refer to your frontend course to load this JS script in your html

  5. here we run vite with the --config /dev/null option to ignore the configuration in vite.config.js, in the next section we will see why this configuration is needed and how it solves the issue at hand

  6. in particular, check the CORSMiddleware in FastAPI, which allows you to configure CORS policies on the backend side, but be careful with it, as a misconfiguration can lead to security vulnerabilities