How to handle Node.js file uploads with Fastify

Written by:
Amarachi Aso
wordpress-sync/feature-fastify-js-upload

December 8, 2022

0 mins read

File uploading is a commonly needed functionality in web applications. Because of this, many web frameworks provide ways to allow server-side applications to accept files and work with them. This is the same with Fastify, which is a Node.js web framework that is often regarded as a successor to Express. It's fast and highly focused on providing the best developer experience with a powerful plugin architecture and the least overhead possible.

In this tutorial, you'll use the Fastify framework in a Node.js application to set up file uploading. You'll learn how file uploads work on a typical server and how to handle file uploads on a Fastify server application.

File uploading basics

If you want to upload files to an application server, a client computer needs to make a POST request to the server. In the request body, it would send data that is typically submitted through HTML forms. HTML forms provide three methods of encoding data:

  • application/x-www-form-urlencoded (default) is the default content-type or encoding for HTML forms. However, when files are submitted using this form, it must be encoded using the multipart/form-data method.

  • multipart/form-data is used to represent the multiple parts or fields present in an HTML form, with the parts sent as name/value pairs in the body of the request. The name and value pair correspond to the name and value of the input fields respectively.

  • text/plain is used to send data in plain unformatted text, and cannot be used to send files.

When files are sent to an application server, the purpose is usually for the server to process the files, save them, or do both. Server applications can process and save uploaded files in a number of ways including:

  • Saving them on the file system of your own server. This can be in a dedicated directory on the same server your application is running on or in a different server dedicated to storing files.

  • Storing them in a database.

  • Uploading the files to cloud storage like Amazon S3 or Cloudinary.

This tutorial focuses on using the first method to handle file uploads.

Implementing file uploading with Fastify

Before you begin this tutorial, you'll need the following:

  • Node.js installed

  • A code editor of your choice, like Visual Studio Code (VS Code)

  • Postman, and you can either sign in and use the web app, or have it installed on your computer.

In the following sections, you'll create a Fastify project, set up a file upload plugin, create an /upload route that can handle multiple file uploads, and write a test case for the route.

Create a project and set up Fastify

To begin, you need to create a project and set up Fastify. Navigate to the directory where you wish to create this project and create a folder called fastify-file-upload. In your terminal, you can run the following command:

1mkdir fastify-file-upload

Change your directory to this folder:

1cd fastify-file-upload

Then open the folder in your preferred code editor. If you're using VS code, you can use the following command:

1code .

Initialize your project and create a package.json file by running the following:

1npm init -y

Then, install the dependencies you'll be using in this project:

1npm install fastify @fastify/multipart form-auto-content tap

And create two files: app.js and server.js:

1touch app.js server.js

app.js will contain your application code. Inside it, copy and paste the following code:

1import Fastify from 'fastify'
2
3export const app = Fastify({
4    logger: true
5  })

import Fastify from fastify

This code imports the Fastify framework and creates an instance of an app from it. Next, put the following code into your server.js file:

1import { app } from './app.js'
2
3const start = async () => {
4    try {
5      await app.listen({ port: 8000 })
6      console.log(`server listening on ${app.server.address().port}`)
7    } catch (err) {
8        app.log.error(err)
9      process.exit(1)
10    }
11  }
12  start()

This code will serve your application when it's run, or it returns an error if a problem occurs while trying to serve it.

In this project, you'll be using ES6 modules syntax, therefore, your JavaScript files need to be loaded as modules. To make sure your JavaScript files are loaded as modules, add the property type to your package.json and set the value to module:

1"type": "module"

Set up a file upload plugin

Fastify-multipart provides support for multipart content-types in Fastify. Since you've already installed the plugin when you installed your dependencies, it's time to set it up. To do this, you need to import multipart from '@fastify/multipart' and then register it.

Add the lines starting with + to the content of your app.js file:

1  import Fastify from 'fastify'
2+ import multipart from '@fastify/multipart'
3
4export const app = Fastify({
5    logger: true
6  })
7
8+ app.register(multipart)

Here you registered multipart on the Fastify app you created earlier. Now your application instance can parse multipart content-type with ease.

File uploads

There are several ways to access the file(s) from a POST request. For example, if you know a request contains only one file, you can access the file with the following code:

1const data = await request.file()

In this code, request is the request parameter from the handler function for your upload route where your handle function looks something like this:

12app.post('/upload', async function (request, reply) {
3
4  const data = await request.file()
5
6  // The rest of your code
7}

If you're expecting multiple files, the difference will be that you would call the files() method instead of file() like so:

1const data = await request.files()

In your app.js file, you need to write code to fully implement multiple file uploading. But first, you need to create a directory where the files uploaded on your application will be stored. Create this directory at the root of your project and name it uploads:

1mkdir uploads

After that, make the following modifications to the app.js file:

1import Fastify from 'fastify'
2import multipart from '@fastify/multipart'
3+ import fs from 'fs'
4+ import util from 'util'
5+ import { pipeline } from 'stream'
6
7+ const pump = util.promisify(pipeline)
8
9export const app = Fastify({
10    logger: true
11  })
12
13  app.register(multipart)
14
15+   app.post('/upload', async function (req, reply) {
16
17+ const parts = req.files()
18+ 
19+ for await (const part of parts) {
20
21+     // upload and save the file
22+     await pump(part.file, fs.createWriteStream(`./uploads/${part.filename}`))
23
24+ }
25
26+     return {message : 'files uploaded' }
27+ })

In the modified code, you imported functions from the core Node.js modules fs, util, and stream. Then you created an /upload route and its handler function.

Inside the handler function, you used req.files() to access all the files from the request. You looped through the files, and for each of the files, the code await pump(part.file, fs.createWriteStream(`./uploads/${part.filename}`)) uploads, and is saved to the upload directory you created earlier.

Please note that the files you're uploading are not accumulated in memory before they're saved. Instead, the fs.createWriteStream function is used to create a stream on the receiving end, while the file stream coming from part.file is piped to the newly created stream. This ensures that the content of the file is not first buffered in memory, rather, chunks of data are being encoded directly into the file where they're saved as they come through the stream. This is especially helpful when uploading large files. It doesn't block the event loop, and it's also a memory efficient way to handle files on your application.

Now it's time to run your server and send files to it using Postman. In your package.json file, replace the start script with the following:

1"start": "node server.js"

Then start the server with the following:

1npm start

Navigate to Postman and make a POST request to http://localhost:8000/upload. Under the body of the request you're about to make, select the form-data option, and upload two files of your choice:

wordpress-sync/blog-fastify-upload-postman
Postman interface for uploading files.

Here, you upload one file named article.txt and another named cover.jpg (their keys will be "article" and "coverImage", respectively). After uploading the files, you should get a status code of 200 and the return object {message : 'files uploaded' }:

wordpress-sync/blog-fastify-upload-field-files
Two files uploaded from Postman.

You should also see the files added to the "upload" directory you created earlier for uploads.

At this point, you've successfully handled multiple file uploads using the fastify/multipart plugin. Most forms require you to provide not only files, but files and other text fields. In order to handle forms like this, you should modify your existing code to handle both file streams and other fields.

To do so, make the following modifications to your app.js file:

1import Fastify from 'fastify'
2import multipart from '@fastify/multipart'
3import fs from 'fs'
4import util from 'util'
5import { pipeline } from 'stream'
6
7const pump = util.promisify(pipeline)
8
9export const app = Fastify({
10    logger: true
11  })
12
13  app.register(multipart)
14
15  app.post('/upload', async function (req, reply) {
16
17+ const parts = req.parts()
18
19for await (const part of parts) {
20
21+  if (part.file) {
22
23    // upload and save the file
24    await pump(part.file, fs.createWriteStream(`./uploads/${part.filename}`))
25
26+  } else {
27+    // do something with the non-files parts
28+  }
29
30}
31
32    return {message : 'files uploaded' }
33})

Notice that this time you're using req.parts() to access both file streams and fields, instead of req.files(). And in the body of the loop, you're using an if statement to determine which part is a file, and to handle them differently from fields.

Now, it's time to make another request. Save your file, then stop and restart your server with npm start. Navigate back to Postman, delete the key/value pairs from the previous request, and create new ones. On the first field, provide your full name, and then provide any two files (as you did previously).

Note: You may want to delete the files that were last uploaded to your upload directory if you intend to use the same files now.

Your request should look like this:

wordpress-sync/blog-fastify-upload-field-files-1
One field and two files uploaded from Postman.

And if it does, your application can now handle multiple file streams and fields.

Write test cases

In order to protect your file upload implementation, you need to write tests for it. But, how can you write test cases for file upload in Fastify? Here, you'll be using the testing framework tap, the package form-auto-content, and  Fastify’s .inject method to write your test cases.

Create the file app.test.js at the root of your project. Then copy and paste the following code into it:

1'use strict'
2
3import { test } from 'tap'
4import formAutoContent from 'form-auto-content'
5import fs from 'fs'
6import { app } from './app.js'
7
8const myForm  = formAutoContent({
9    fullName: 'Amarachi Aso',
10    article: fs.createReadStream(`./sample_files/file.txt`),
11    coverImage: fs.createReadStream(`./sample_files/cover.jpg`)
12  })
13
14test('submits form with multiple files in it', async t => {
15
16    const res = await app.inject({
17        method: 'post',
18        url: '/upload',
19        ...myForm
20      })
21  t.equal(res.statusCode, 200, 'returns a status code of 200')
22})

Replace the file paths ./sample_files/file.txt and ./sample_files/cover.jpg with file paths to any two files in your filesystem. Or you can make a copy of these files from here.

The Fastify .inject method allows you to inject fake HTTP requests to your application without running your code on an actual server. In the previous code, notice how you used formAutoContent to create a form called myForm and then used this form as the request body for a fake POST request inside your test case. You only have one test case, and it expects that when it submits a form with multiple files in it, the application should return a statusCode of 200.

Run this test and confirm that it passes. In your package.json file, replace the test script with this one:

1"test": "tap --reporter=list --watch",

Then run the following:

1npm test

Your test should pass and the output should look like this:

wordpress-sync/blog-fastify-upload-term
Passing test.

All the code for this tutorial is available on this GitHub repo.

Conclusion

In this article, you learned about the basics of file uploading and why forms containing files are only encoded using the multipart/form-data content-type. You also learned about the two ways that server applications can process and save files either in a dedicated directory on the same server or uploaded to a cloud storage like Amazon S3. After that, you learned how to handle uploads in Fastify, a fast and low overhead web framework for Node.js

If you're interested in learning more, check out the documentation for the Fastify-multipart plugin and from the Fastify guide on testing.

Posted in:Engineering

Snyk is a developer security platform. Integrating directly into development tools, workflows, and automation pipelines, Snyk makes it easy for teams to find, prioritize, and fix security vulnerabilities in code, dependencies, containers, and infrastructure as code. Supported by industry-leading application and security intelligence, Snyk puts security expertise in any developer’s toolkit.

Start freeBook a live demo

© 2024 Snyk Limited
Registered in England and Wales

logo-devseccon