Server-side forms

Learn how to build interactive websites using forms and Node

  • js
  • html
  • http

Forms are the building blocks of interactivity on the web. Until client-side JavaScript gained the ability to make HTTP requests (in the early 2000s) forms were the only way to send user-generated data from the browser to the server. They’re still the simplest and most robust way to send data, since they work even if your JS fails to load, is blocked, or errors.

Setup

  1. Download the starter files
  2. cd into the workshop/ directory
  3. npm install to install the dependencies
  4. npm run dev to start the server with nodemon. This is a helper that auto-restarts your server when you save changes

Before we get stuck into forms lets practice our Express basics. Open workshop/server.js in your editor. You should see a server that listens on port 3333. There’s also an object containing data about different dogs imported from dogs.js.

Challenge 1: server setup

  1. Add a route for the homepage (GET /)
  2. Return an HTML string containing a <ul>
  3. Each <li> in the list should contain a dogs name

Hint

You can use Object.values(myObj) to get an array of all the values. You can then generate a dynamic list by looping over that array:

const myObj = {
first: { test: "hi" },
second: { test: "bye" },
};
let items = "";
for (const thing of Object.values(myObj)) {
items += `<li>${thing.test}</li>`;
}
const list = `<ul>${items}</ul>`;

You could also combine array.map and array.join to create the string.

When you’re done you should be able to visit http://localhost:3333 and see the list of dogs rendered.

Toggle answer
server.get("/", (request, response) => {
let items = "";
for (const dog of Object.values(dogs)) {
items += `<li>${dog.name}</li>`;
}
const html = `
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Dogs!</title>
</head>
<body>
<ul>
${items}</ul>
</body>
</html>
`
;
response.send(html);
});

GET requests

Browsers support two types of HTTP requests (without JS): GET and POST. When a user navigates (either by clicking a link or typing a URL into the address bar) the browser will send a GET request, then render the response. There are also certain HTML tags that trigger GET requests for a resource to display within the page, e.g. <img>.

Forms can also make GET requests. Here’s an example form:

<form>
<input name="myMessage" />
<button>Submit</button>
</form>

By default a form sends a GET request to the current page (when submitted). It will find all the inputs within the form and add them into the “search” part of the URL (the bit after the ?). Assuming this form was rendered on example.com clicking the submit button would send this request:

GET example.com?myMessage=whatevertheusertyped HTTP/1.1

Each input is added to the search string in this format: ${inputName}=${inputValue}. If you don’t add a name attribute the input won’t be submitted.

There’s nothing special about the request: we could have achieved the same result by creating a link like this:

<a href="?myMessage=whatevertheusertyped">Click me</a>

The advantage of a form is the search part of the URL is dynamic—it is typed by the user, not hard-coded into the HTML by the developer.

Forms like this are mostly used for implementing search functionality. The GET method is for retrieving resources. It shouldn’t be used for creating/updating/deleting things, since browsers treat GETs differently (e.g. they cache them).

Let’s add some search functionality to our dogs page. Express automatically parses the “search” part of the URL for each request. You can access this object at request.query. For example our request above would result in a query object like:

{ myMessage: "whatevertheusertyped", }
  1. Add a search form to the homepage (with a single input)
  2. Retrieve the user-submitted value on the server
  3. Filter the list of dogs based on the user-submitted value
  4. Make sure the full list still displays if there’s no search value

E.g. if the user searches for “o” the list should only include “rover” and “spot” (since they both contain the letter “o”).

Hint

You can use string.includes to check if a string contains a given substring. E.g.

const search = "rov";
const name = "rover";
const match = name.toLowerCase().includes(search.toLowerCase());
if (match) {
items += `<li>${name}</li>`;
}

Don’t forget this is case sensitive!

When you’re done you should be able to submit the form to filter the list of dogs on the page.

POST requests

Forms can also send POST requests, which allows users to create or change data stored by the server. You can make a form send a POST by setting the method attribute.

<form method="POST">
<input name="myMessage" />
<button>Submit</button>
</form>

Note: forms cannot use any other HTTP methods. This means we’ll be using POST for creating, updating and deleting things.

A POST request doesn’t include information in the URL. Instead it puts it in the request body. This means it’s not directly visible to the user, and won’t be logged or cached by things that store URLs. The information will be formatted in the same way as a GET, it’s just sent in a different place. E.g.

POST example.com HTTP/1.1

myMessage=whatevertheusertyped

Since request bodies are sent in lots of small chunks (as they can sometimes be very large) our server doesn’t get it all in one go. This means you must use the built-in Express middleware for parsing request bodies. You can refer back to our Express introduction workshop to see exactly how.

Challenge 3: add a dog

Let’s add a form for submitting new dogs to the site. We aren’t using a database to store our dogs persistently, so we’ll just store new dogs by adding them into the dogs object in-memory. This means the dogs will reset each time the server restarts.

Note: it’s important to always redirect after a POST request. This ensures the user only ever ends up on a page rendered via a GET. Otherwise if the user navigated back to the results page their browser would resend the POST and you’d get a double-submission. This is why lots of sites say “Don’t click back or you’ll be charged twice”!

  1. Add a new route GET /add-dog
  2. It should render another form with inputs for each property of a dog
  3. Add a new route for POST /add-dog
  4. It should use the Express body-parsing middleware to access the submitted body
  5. Add the new dog to the dogs object
  6. Redirect back to the homepage so the user can see their new dog in the list

When you’re done you should be able to visit http://localhost:3333/add-dog, submit the information for a new dog, then be redirected to the homepage and see that information in the list.

Deleting resources

So far we’ve only had one form per page. Each form has just submitted to the default URL—the current one. However if you want to use multiple forms on a page they’ll need different URLs to represent different actions.

For example we might want to be able to delete dogs from our homepage. This action might happen via the /delete-dog URL. We can tell the form to send its request to this URL with the action attribute:

<form action="/delete-dog" method="POST"></form>

But how will our /delete-dog endpoint know which dog to delete? We need the request body to contain the name of the dog to remove, like this:

POST /delete-dog HTTP/1.1

dogName=pongo

We could have the user type the name in, but that’s not a great experience. It would be better if each “delete button” could be a separate form with a hard-coded “name to delete”. That way the user can just click a button to send the delete request.

There are two ways to hard-code data into a form. You can use inputs with type="hidden". These aren’t displayed to the user but will still be submitted to your server.

<form action="/delete-dog" method="POST">
<input type="hidden" name="dogName" value="pongo" />
<button>Delete</button>
</form>

You can also set name and value attributes on directly on button elements. When that button is used to submit the form those values will be submitted in the request body.

<form action="/delete-dog" method="POST">
<button name="dogName" value="pongo">Delete</button>
</form>

It’s like a little self-contained form. The only thing the user sees is the button to click.

Challenge 4: removing dogs

Let’s add delete buttons next to each dog in the list on the homepage. You can remove a dog from the dogs object using the delete operator. E.g.

const name = "pongo";
delete dogs[name];
  1. Add a delete form next to each dog’s name on the homepage
  2. Each one should send a POST to /delete-dog with the name of the dog to remove in the body
  3. Add a new route POST /delete-dog
  4. It should get the name of the dog to remove from the request body
  5. Use the name to remove the dog from the dogs object
  6. Redirect back to the homepage so the user can see the dog is gone

When you’re done you should be able to click the delete button next to each dog and see that dog disappear from the list.

Modularisation

Our server.js file is starting to get a little cluttered. We’ve got handlers and logic for several different routes, plus the code that starts our server listening. It’s not too hard to follow right now, but as an application grows you’ll want to split things up into separate files.

You can import and register route handler functions like this:

const someHandler = require("./someHandler.js");

server.get("/hello", someHandler);

There are different philosophies on modularisation. Some people like dividing things up by the type of code. For example put all the route handlers in one place, all the database queries in another place, and all the HTML templates in another place. So you might have folders like handlers/, database/ and templates/. A single feature “add a dog” might be divided across all three folders.

Other people like to divide code up by features. For example put all the code related to a single feature (like “adding a dog”) into a single file. So you might just have a routes/ folder containing addDog.js. This file would contain everything required for that feature—the route handlers, data access and HTML strings all together.

Challenge 5: modularise your server

Pick one of the methods above and move your route handlers out of server.js. Don’t forget to import/export everything!

Stretch goal: dog pages

It would be nice if each dog had its own page that showed all the information about it. For example GET /dogs/pongo would show information about Pongo. You can achieve this with dynamic route paths.

  1. Add a single extra route that can render any dog’s page
  2. It should respond with HTML containing all the info about that dog
  3. Add a link for each dog on the homepage so you can click through to each page

Once that’s done it would be a better experience if the user was redirected to the relevant dog page after creating a new dog. For example if they created a new dog named “Bilbo” they should be redirected to /dogs/bilbo to see all the info about that dog.

  1. Amend your POST /add-dog handler to redirect to the relevant dog’s page

Stretch goal: server-side validation

It’s important to check user-submitted information. It could be missing, incomplete, or even malicious. For example right now you can submit the “add dog” form with empty inputs, which results in the other pages looking broken.

Client-side validation can help (e.g. adding required or pattern to inputs), however it’s easy to bypass (e.g. by editing the elements in dev tools).

You should always validate your data on the server, since that’s the only place you can trust.

  1. Amend your POST handlers to check your data is valid
  2. If the data is not valid redirect the user to a generic error page
  3. Create a generic error route that tells the user something went wrong

It would be a better user-experience to send the user back to the same page, but highlight which inputs had validation errors. Unfortunately that’s a little more complex. HTTP requests are “stateless”, which means we can’t distinguish a normal GET /add-todo from a redirect to GET /add-todo after an error.

We can use cookies to store information between requests. We could store the validation errors in a cookie before redirecting, then use that info to render error messages in the form. We’ll be looking at cookies in a later workshop, but feel free to attempt this now if you want a challenge.