DEV Community

Pascal Lleonart
Pascal Lleonart

Posted on

How to upload files to a server in NodeJS with Formidable

In this blog post, you'll discover Formidable, a light and efficient package for form and file uploading handling.

Project setup

We need to create a new project, so

npm init -y
Enter fullscreen mode Exit fullscreen mode

Then install Formidable:

pnpm add formidable
Enter fullscreen mode Exit fullscreen mode

Note: don't forget to setup the project for ESM (add "type": "module", into your package.json).

Now let's create our app!

Upload

Create a new index.js file:

import http from 'node:http'
import formidable, {errors as formidableErrors} from 'formidable'

const FORM_TEMPLATE = `
  <form action="/upload" enctype="multipart/form-data" method="post">
    <input type="text" name="filename" />
    <input type="file" name="files" multiple="multiple" />
    <input type="submit" value="Upload" />
  </form>
`

createServer((req, res) => {
  if (req.url === '/upload' && req.method.toLowerCase() === 'post'){
    const form = formidable({
      // we'll put our formidable config later
    })
    let fields
    let files
    try {
      [fields, files] = await form.parse(req)
    } catch (err) {
      console.error(err)
      res.writeHead(err.httpCode || 400, { 'Content-Type': 'text/plain' })
      res.end(err.message)
      return
    }
    res.writeHead(200, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ fields, files }))
    return
  }

  // show the form
  res.writeHead(200, { 'Content-Type': 'text/html' })
  res.end(FORM_TEMPLATE)
})
  .listen(3000)
Enter fullscreen mode Exit fullscreen mode

We are going to configure our Formidable instance.

const form = formidable({
  uploadDir: 'uploads',
  keepExtensions: true,
  filename: (name, ext, part, form) => {
    // don't forget to import `randomBytes` from `crypto` module
    return `${name}.${randomBytes(8).toString('base64url')}${ext}`
  }
})
Enter fullscreen mode Exit fullscreen mode

We are putting a random file id that will permit users to upload files with the same name without overriding them.

Error handling

In the catch, you can decide to put a custom error message on a specific error.

    // ...

    } catch (err) {

      console.error(err)      
      res.writeHead(err.httpCode || 400, { 'Content-Type': 'text/plain' })

      if (err.code === formidableErrors.maxFieldsExceeded) {
        res.end("max fields exceeded")
        return
      }

      res.end(err.message)
      return
    }

    // ....
Enter fullscreen mode Exit fullscreen mode

View file

Add this line on the top of your server handling function:

const url = new URL(req.url, `http://${req.headers.host}`)
Enter fullscreen mode Exit fullscreen mode

This will serve to parse our url path, params, etc...

Now, after our if (req.url === "/upload" ...), put this condition:

if (url.pathname === "/file") {
  const filepath = url.searchParams.get("path")
  const fileContent = await readFile(`./uploads/${filepath}`)
  res.writeHead(200, { 'Content-Type': "text/plain" })
  res.end(fileContent)
  return
}
Enter fullscreen mode Exit fullscreen mode

You need to import readFile from fs/promises.

But we have a little problem: if we have an image, for example, the Content-Type of the response won't match if the current response's body format. To avoid this we need to have a new function that will search for the format of a file depending on its extension.

Response & file format matching

Create a new file named content-types.json:

{
    "jar": "application/java-archive",
    "ogg": "application/ogg",
    "pdf": "application/pdf",
    "xhtml": "application/xhtml+xml",
    "json": "application/json",
    "app/xml": "application/xml",
    "zip": "application/zip",

    "gif": "image/gif",
    "jpeg": "image/jpeg",
    "png": "image/png",
    "tiff": "image/tiff",
    "svg": "image/svg+xml",

    "multipart/mixed": "multipart/mixed",
    "multipart/alternative": "multipart/alternative",
    "multipart/related": "multipart/related (using by MHTML (HTML mail).)",
    "multipart/form-data": "multipart/form-data",

    "css": "text/css",
    "csv": "text/csv",
    "html": "text/html",
    "js": "text/javascript",
    "txt": "text/plain",
    "xml": "text/xml",

    "mpeg": "video/mpeg",
    "mp4": "video/mp4"
}
Enter fullscreen mode Exit fullscreen mode

These are some file formats, if you want, add others...

Then, create a new function called getFileFormat in your index.js file:

async function getFileFormat(filename) {
  const supportedContentTypes = JSON.parse(await readFile("./content-types.json", "utf8"))

  const ext = filename.split('.')[filename.split('.').length-1]

  return supportedContentTypes[ext] ?? "text/plain"
}
Enter fullscreen mode Exit fullscreen mode

This function will get the content of content-types.json and will get the associated format. If it's undefined it will return the default text/plain.

Then you need to edit your http Content-Type header:

if (url.pathname === "/file") {
  // ...

  res.writeHead(200, { 'Content-Type': await getFileFormat(filepath) })

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Testing

Now you can test your application!

To get the upload form, visit /, that will return you to /upload that tell you some informations about the POST request (where the current file is saved). Then you can get the file name and go to /file?path=<the file name> to get the file in your browser.

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

Top comments (0)

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay