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.

Top comments (0)