In this lesson, we are going to cover how to upload files to the server using the multipart/form-data
encoding, which we discussed in the previous lesson.
Updated database and model
First, you need a new structure for our users
table. Since we haven't discussed how to run database migrations yet, let's delete our old database.sqlite
and create a new one.
Next, add a new picture
column in our users
table.
models/user.js
1db.serialize(() => {
2 db.run(
3 `CREATE TABLE IF NOT EXISTS users (
4 id INTEGER PRIMARY KEY AUTOINCREMENT,
5 username TEXT,
6 email TEXT,
7 picture TEXT
8 )`
9 );
10});
You should know that the database cannot store the actual image file. The files should be saved inside the server's file system, and this picture
column will simply store the path to the image file.
The corresponding create()
method will become:
1static create(username, email, filePath, callback) {
2 const sql = "INSERT INTO users (username, email, picture) VALUES (?, ?, ?)";
3 db.run(sql, [username, email, filePath], function (err) {
4 callback(null, this.lastID);
5 });
6}
New form
Next, our user creation form also requires some modification.
views/index.pug
1extends layout.pug
2
3block meta
4 title Home
5
6block content
7
8 form(id="myForm")
9 label(for="username") Username:
10 input(type="text", name="username", id="username", value="")
11 label(for="email") Email:
12 input(type="email", name="email", id="email", value="")
13 label(for="picture") Picture:
14 input(type="file", name="picture", id="picture", value="")
15 input(type="submit", value="Submit")
16
17 script.
18 . . .
This template will be rendered into:
1<form id="myForm">
2 <label for="username">Username:</label>
3 <input type="text" name="username" id="username" value="" />
4
5 <label for="email">Email:</label>
6 <input type="email" name="email" id="email" value="" />
7
8 <label for="picture">Picture: </label>
9 <input type="file" name="picture" id="picture" value="" />
10
11 <input type="submit" value="Submit" />
12</form>
In this case, we are going to use JavaScript and the fetch()
method to submit this form.
1document.addEventListener("DOMContentLoaded", function () {
2 const form = document.getElementById("myForm");
3 const usernameInput = document.getElementById("username");
4 const emailInput = document.getElementById("email");
5 const pictureInput = document.getElementById("picture");
6
7 const formData = new FormData();
8
9 usernameInput.addEventListener("input", function () {
10 formData.set("username", usernameInput.value);
11 });
12
13 emailInput.addEventListener("input", function () {
14 formData.set("email", emailInput.value);
15 });
16
17 pictureInput.addEventListener("change", function () {
18 formData.set("picture", pictureInput.files[0]);
19 });
20
21 form.addEventListener("submit", async function (event) {
22 event.preventDefault(); // Prevent the default form submission
23
24 await fetch("/users", {
25 method: "POST",
26 body: formData,
27 });
28 });
29});
Line 7, since we are submitting the form in multipart/form-data
encoding, you must use FormData()
instead of URLSearchParams()
.
Line 18, pictureInput.files[0]
accesses the first file from an array of files. The reason that this is necessary is because the same <input>
element can be used to upload multiple files by adding a multiple
attribute.
Also, notice that we did not include a custom header in the fetch()
method. That is because when using FormData()
, fetch()
can automatically handle the Content-Type
header. Manually setting it could lead to potential errors in this case.
Updated controller
Lastly, we also need to consider how can Express process the submitted data in the backend. Unlike the application/x-www-form-urlencoded
encoding, Express does not support multipart/form-data
natively. Instead, you'll need to rely on an external package called multer
.
Install the package with the following command:
1npm install multer
And then, go to index.js
. This is where you need to configure multer
and add it as a middleware.
1import multer from "multer";
2
3const storage = multer.diskStorage({
4 destination: function (req, file, callback) {
5 callback(null, "uploads/"); // Configures the upload path
6 },
7 filename: function (req, file, callback) {
8 callback(null, Date.now() + "-" + file.originalname); //Configures the custom file name
9 },
10});
11
12const upload = multer({ storage: storage });
Also remember to create the corresponding uploads directory.
1mkdir uploads
Next, add upload
as a middleware.
1app
2 .route("/users")
3 .get(userController.getAllUsers)
4 .post(upload.single("picture"), userController.createNewUser); // <==
The single()
method means that we are only uploading one file.
And finally, update the createNewUser()
controller method.
controllers/userController.js
1createNewUser: async function (req, res) {
2 const { username, email } = req.body;
3 const file = req.file;
4
5 User.create(username, email, file.path, (err, userID) => {
6 res.redirect(303, `/users/${userID}`);
7 });
8},
Start the dev server, and go to http://localhost:3001
.
Submit the form, and a new file should appear under the uploads
directory.
1.
2├── controllers
3│ └── userController.js
4├── database.sqlite
5├── index.js
6├── models
7│ └── user.js
8├── package.json
9├── package-lock.json
10├── uploads
11│ └── xxxx-xxxx.jpg
12└── views
13 ├── index.pug
14 ├── layout.pug
15 └── user.pug
And inside the users table, a new user record should be created. Its picture
key should point to the uploaded file.