DEV Community

Cover image for Persist data in Vercel(NextJS) serverless backends
ogbotemi-2000
ogbotemi-2000

Posted on

Persist data in Vercel(NextJS) serverless backends

Photo by Aron Visuals on Pexels, chosen for its similarity to Vercel's logo

TL:DR;

  • This is not an introduction however, here is a good intro to serverless backends on Vercel by Vishal Yadav
  • Persisting data by storage in global objects doesn't work in Vercel functions because they are ephemeral.
  • Writing files to the directory of the project throws a server crashing error while reading files doesn't
  • The workaround used involves writing and reading files with unique filenames to a temporary directory; /tmp, allowed by Vercel but absent in the docs

I recently migrated my web app from Railway to Vercel only to encounter bugs in my Node.js server due to the short-lived nature of Vercel functions.
I had used an object to store intermediate data for the url parameter of each GET request. The said object reverts back to {} everytime the serverless function is hit.

The following comprises the steps I took in working up a solution as well a link to the web app to see a working version of the code and logic below.

Figuring out likely problems and their solutions

Unique filenames, unique data

Filenames for stored data should be unique to avoid mixing them for every request.
Say the exported function in api/root.js is reached by requests to https://<unique-url-to-app>/api/root. It may at first suffice to generate a unique string from the parameters of the GET or POST requests however, serializing request URLs into unique filenames leaves room for duplication not to mention, frustrations with invalid strings for filenames.

One solution to generating UUIDs is require('crypto').randomUUID() if it is supported by the current node version you choose to work with.
Otherwise, here is a useful function for generating UUIDs gotten from the 1loc repo

const uuid = (a) =>a
  ? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16)
  : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);
Enter fullscreen mode Exit fullscreen mode

Making reading/writing data similar to using objects: {"data":"data"}

The code design is done in a way that is asynchronous yet ergonomic in the way getting and setting properties on objects are.

  • Writing data is done after which a callback is supplied the generated UUID wherein it may be sent to the client as part of the response
/* api/store.js */
let fs   = require('fs'),
    path = require('path'),
    dir  = '/tmp/',
    uuid = a =>a
      ? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16)
      : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid);

module.exports = {
  write: (data, cb, id)=>fs.writeFile(data, path.join(dir, id=uuid()),
    function(err) {
      if(err) throw err;
      else cb(id)
  })
}
Enter fullscreen mode Exit fullscreen mode

Using it is as follows; reading the data comes after writing it

let {write} = require('./api/store'),
    data;

write(data, function(uuid){
  /*the uuid is sent to the client which then uses it as a sort of unique string to retrieve its associated data by sending it along with its requests at a later time to be read by store.read */
  response.send(JSON.stringify({uuid})/*may be sent as either JSON or text*/)
});
Enter fullscreen mode Exit fullscreen mode
  • Reading of stored files is wrapped with a Promise to await the result of its callback as follows:
/* api/store.js */
let fs   = require('fs'),
    path = require('path'),
    dir  = '/tmp/';

module.exports = {
  read: uuid=>new Promise((resolve, reject)=>{
    fs.readFile(path.join(dir, uuid), (err, buffer)=>{
      if(err) reject(err);
       else resolve(buffer.toString('utf-8'))
    })
  }),
  rm: id=>fs.unlinkSync(path.join(dir, id))
}
Enter fullscreen mode Exit fullscreen mode

Which can then be used as follows

let {read, rm} = require('./api/store'),
    data;

if(data = await store.read(/*uuid*/)) {

  response.send(data);
  rm(/*uuid*/) //may be removed or left alone
}
Enter fullscreen mode Exit fullscreen mode

Final and working draft of the code in api/store.js

/* store.js */
let fs     = require('fs'),
    uuid   = (a) => (a ? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16) : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, uuid)),
    /* may be made asynchronous */
    rm     = id=>fs.unlinkSync(path.join(dir, id)),
    dir    = '/tmp/',
    path   = require('path');

/*writes/reads data as text/plain, open to modifications*/
module.exports = {
  read: id =>new Promise((resolve, reject)=>fs.readFile(path.join(dir, id), (err, buffer)=>{
      if(err) reject(err);
      else resolve(buffer.toString())
    })
  ),
  write: function(data, cb, id) {
    fs.writeFile(path.join(dir, (id=uuid())), data, _=>cb(id))
  },
  rm 
}
Enter fullscreen mode Exit fullscreen mode

Using it in the wild

Here is how I used it to store and retrieve results generated for each valid request in this webapp

let store  = require('./store');
module.exports = async function(request, response, uuid) {
  let {url, id} = request.query;
  if(uuid = await store.read(id)) {
    /* exists */
    response.send(/* data */),
    /* you may remove the file or leave it since the contents of the /tmp/ directory are likely temporary */
    store.rm(uuid)
  } else {
    store.write('data', function(id) {
       /* send the unique id for the stored data in some way as JSON or text */
       response.send(id)
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The /tmp/ directory, from its name, may be susceptible to changes from other programs on the server and therefore is not advisable to persist data in it in the manner you would a database.

Vercel provides object blobs for persistent storage but it is not used in this code base since its need for storage is temporary and only throughout the short runtime of the serverless function which is expected to send a reponse within a maximum allowable time of 25s if its runtime is edge.

This maximum duration is configurable if the runtime of the function is set to something aside edge like nodejs as follows

export const runtime = 'nodejs';
export const maxDuration = 15;
/*rest of the code in the file goes here*/
Enter fullscreen mode Exit fullscreen mode

Serveless functions are a great way to start building the backend for a website or webapp with less worrying over edge networks, scaling, server distribution, IP-based content delivery, etcetera

Top comments (0)