Categories
Express JavaScript Nodejs

Using Template Engines with Express

To render an HTML response with an Express route, we should return it as a template with the variable interpolated rather than return it directly as an HTML string.

Templates are convenient and we can use them in a much more flexible way than HTML strings.

In this article, we’ll look at how to set the template engine of our app, and returning HTML responses.

Choosing Template Engines

We can use popular template engines with Express, like Pug, Mustache, and EJS. The default for the Express application generator is Jade, but we can use the other ones listed as well.

To render template files, we can set the application settings. To set the templates folder, we can set the views property by writing the following:

app.set('views', './views')

It’ll set the templates folder to the views directory of our app.

Also, we can set the view engine we want to use by setting the view engine property as follows:

app.set('view engine', 'pug')

To install the template engine library, we can run something like the following:

npm install pug --save

The command above will install pug view engine for us to use.

Rendering Items with Template Engines

We can use the res.render method to render our variables to our templates.

First, we create the views folder to store our templates as we set the ./views folder to be our templates folder.

Since we chose to use Pug as our template engine, the template files should have the .pug extension.

Inside the views folder, create an index.pug and then put:

html  
  head  
    title= title  
  body  
    h1= message

The title= and h1= are our tags and title and message are keys in which Express will use to get the values from in the object that we’ll pass to res.render .

Then we can create index.js in the same level as the views folder and add:

const express = require('express')  
const app = express()  
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))  
app.set('views', './views');  
app.set('view engine', 'pug');

app.get('/', (req, res, next) => {  
  res.render('index', { pageTitle: 'Hello', pageMessage: 'Hello there!' });  
})

app.listen(3000, () => console.log('server started'));

In the code above, we called res.render to render an HTML response. The object:

{ pageTitle: 'Hello', pageMessage: 'Hello there!' }

will be passed to our index.pug template.

title will be the title in:

html  
  head  
    title= pageTitle  
  body  
    h1= pageMessage

message will be replaced by the 'Hello there!' . Anything right of the tag and the equal sign will be replaced by the value with the key given above.

In other words, pageTitle and pageMessage properties’ values’ are the ones that’ll go in between the title and h1 tags respectively.

So we get

and the title of our page will be 'Hello' .

Pug Syntax

We can have dynamic expressions other than variables. Loops and conditional expressions are part of Pug.

HTML Tags

Other HTML tags can be used with Pug.

For example, a link can be written as follows:

a(href='http://medium.com') Medium

In the code above Medium goes between the a tags and href=’http://medium.com' is the href attribute and value respectively.

Loops

We can also write loops with Pug. For example, if we have:

const express = require('express')  
const app = express()  
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))  
app.set('views', './views');  
app.set('view engine', 'pug');

app.get('/', (req, res, next) => {  
  res.render('index', { pageTitle: 'Hello', pageMessages: ['Hi', 'Bye'] });  
})

app.listen(3000, () => console.log('server started'));

in index.js and:

html  
  head  
    title= pageTitle  
  body  
    ul  
      each pageMessage in pageMessages  
        li= pageMessage

in views/index.pug , then we would get:

Conditionals

We can also add conditional expressions to our Pug template. For example, if we have:

const express = require('express')  
const app = express()  
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))  
app.set('views', './views');  
app.set('view engine', 'pug');

app.get('/', (req, res, next) => {  
  res.render('index', { pageTitle: 'Hello', user: { type: 'member' }   });  
})  
app.listen(3000, () => console.log('server started'));

in index.js and:

html  
  head  
    title= pageTitle  
  body  
    if user.type === 'anonymous'  
      p User type is anonyomous  
    else if user.type === 'member'  
      p User type is member  
    else  
      p User type is admin

in views/index.pug , then we get:

'User type is member'

since user.type in res.render is set to 'member' .

Conclusion

Rendering HTML response is easy if we use a template engine to do it. We can pass in an object in the second argument of res.render to pass in values that we want to display in the template.

With template engines, we can include loops, conditionals, and variables to make displaying dynamic content a breeze.

Categories
Express JavaScript Nodejs

Writing Express Middleware

Middleware functions are functions that have access to the request and response objects, and the next function for call the next middleware.

In this article, we’ll look at what Express middleware does and how we can write them.

Characteristics of Middleware

Middleware functions can run any code, make changes to the request and response object, end the request-response cycle, and call the next middleware in the stack.

A middleware may look something like the following code:

app.get('/', (req, res, next) => {  
  next();  
});

The code above has the request method in which the middleware will be called, which is the get in app.get .

Then we have the '/' which is the route path.

Finally, the middleware function that we pass in has the request and response objects as the first 2 parameters respectively, and the next function, which we call to run the next middleware.

Example

For example, we can write a middleware to log some output when we make a request to the / route.

We can write this as follows:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res, next) => {  
  console.log('middleware called');  
  next();  
});

app.get('/', (req, res) => {  
  res.send();  
})

app.listen(3000, () => console.log('server started'));

next will call our route handler for the / route.

We should get middleware called from the console.log output and an empty response.

To load the middleware for all routes, we can use app.use instead of app.METHOD where METHOD is the request method is in lower case.

For example, we can write an app-wide middleware as follows:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.use((req, res, next) => {  
  console.log('middleware called');  
  next();  
});

app.get('/', (req, res) => {  
  res.send();  
})

app.listen(3000, () => console.log('server started'));

We should get the same thing as before, but if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.use((req, res, next) => {  
  console.log('middleware called');  
  next();  
});

app.get('/', (req, res) => {  
  res.send();  
})

app.post('/foo', (req, res) => {  
  res.send('foo');  
})

app.listen(3000, () => console.log('server started'));

We get middleware called when we make a GET request to / and a POST request to /foo .

Modifying the Request and Response Objects

We can attach new properties and set values for the request and response objects.

For example, we can write:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.use((req, res, next) => {  
  req.requestTime = Date.now();  
  next();  
});

app.get('/', (req, res) => {  
  res.json(req.requestTime);  
})

app.post('/foo', (req, res) => {  
  res.json(req.requestTime);  
})

app.listen(3000, () => console.log('server started'));

Then we get the timestamp of when the request is made when we make a GET request to / and a POST request to /foo .

Likewise, we can do something similar with the response object:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.use((req, res, next) => {  
  res.responseTime = Date.now();  
  next();  
});

app.get('/', (req, res) => {  
  res.json(res.responseTime);  
})

app.post('/foo', (req, res) => {  
  res.json(res.responseTime);  
})

app.listen(3000, () => console.log('server started'));

We also will get the timestamp of when the response is made when we make a GET request to / and a POST request to /foo .

Configurable Middleware

We can make a function that has optional parameters and return a middleware function to create a configurable middleware function.

For example, we can write one as follows:

const express = require('express')  
const app = express()  
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

const configurableMiddleware = (options) => {  
  return (req, res, next) => {  
    req.options = options;  
    next();  
  }  
}

app.use(configurableMiddleware({ date: new Date(), foo: 'bar' }));

app.get('/', (req, res) => {  
  res.json(req.options);  
})

app.listen(3000, () => console.log('server started'));

The configurableMiddleware function takes an options object as a parameter and then return a middleware function with the req.options property set to the options parameter.

Then when we make a request to the / route then we get:

{"date":"2019-12-23T22:37:04.927Z","foo":"bar"}

as the response.

Conclusion

We can use Express middleware functions to run code before a route handler or another middleware is run.

To create a middleware function, we just have to create a function with the request and response objects as the first 2 parameters and the next function as the third parameter.

We can modify the request and response objects by adding new properties to it and set a value for them.

Then we call next to call the next middleware or route handler.

We can create configurable middleware by creating a function that takes an options parameter and returns a middleware function.

Categories
Express JavaScript Nodejs

Basic Routing with Express

Routing is the most important part of a back end application. Express allows us to route URLs to our route handler code easily.

In this article, we’ll look at how to create basic routes with Express.

Basic Routing

Routing is where an Express application response to a client request from a URL or path and a specific HTTP request method, like GET or POST.

Each route in Express can have one more handler function, which are executed when the route is matched.

The general definition of a route takes the following format:

app.METHOD(PATH, HANDLER);

app is the Express app instance.

The METHOD is an HTTP request method in lowercase. Possible methods include GET, POST, PUT, and DELETE.

PATH is the path for the route. HANDLER is the handler function that’s run when the route is matched.

For example, we can write:

app.get('/', (req, res) => {  
  res.send('Hello World!')  
})

To display 'Hello World!' on the screen.

If we want our app to accept a POST request, we can use app.post as follows:

app.post('/', (req, res) => {  
  res.send('Received POST request');  
})

We can test this with an HTTP client like Postman by send a POST request to the URL that our app is running on. Then we should get:

Received POST request

In the response body.

Likewise, we can do the same for PUT and DELETE requests as follows:

app.put('/', (req, res) => {  
  res.send('Got a PUT request at /user')  
})

app.delete('/', (req, res) => {  
  res.send('Got a DELETE request')  
})

Notice that in each route handler, we have a req and res parameter. The req has the request object which has the URL, headers and other fields.

The res object lets us render a response back to the client-side.

Request Object

The req parameter we have in the route handlers above is the req object.

It has some properties that we can use to get data about the request that’s made from the client-side. The more important ones are listed below.

req.baseUrl

The req.baseUrl property holds the base URL of the router instance that’s mounted.

For example, if we have:

const express = require('express');
const app = express();  
const greet = express.Router();  
greet.get('/', (req, res) => {  
  console.log(req.baseUrl);  
  res.send('Hello World');  
})

app.use('/greet', greet);

app.listen(3000, () => console.log('server started'));

Then we get /greet from the console.log .

req.body

req.body has the request body. We can parse JSON bodies with express.json() and URL encoded requests with express.urlencoded() .

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))
app.post('/', (req, res) => {  
  res.json(req.body)  
})

app.listen(3000, () => console.log('server started'));

Then when we make a POST request with a JSON body, then we get back the same that we sent in the request.

req.cookies

We can get cookies that are sent by the request with the req.cookies property.

req.hostname

We can get the hostname from the HTTP header with req.hostname .

When the trust proxy setting doesn’t evaluate to false , then Express will get the value from the X-Forwarded-Host header field. The header can be set by the client or by the proxy.

If there’s more than one X-Forwarded-Host header, then the first one will be used.

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {  
  res.json(req.hostname)  
})

app.listen(3000, () => console.log('server started'));

Then we get the domain name that the app is hosted in if there’re no X-Forwarded-Host headers and trust proxy doesn’t evaluate to false .

req.ip

We can get the IP address that the request is made from with this property.

req.method

The method property has the request method of the request, like GET, POST, PUT or DELETE.

req.params

params property has the request parameters from the URL.

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/:name/:age', (req, res) => {  
  res.json(req.params)  
})

app.listen(3000, () => console.log('server started'));

Then when we pass in /john/1 as the parameter part of the URL, then we get:

{  
    "name": "john",  
    "age": "1"  
}

as the response from the route above.

req.query

The query property gets us the query string from the request URL parsed into an object.

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {  
  res.json(req.query)  
})

app.listen(3000, () => console.log('server started'));

Then when we append ?name=john&age=1 to the end of the hostname, then we get back:

{  
    "name": "john",  
    "age": "1"  
}

from the response.

Response Object

The response object has some useful methods to let us return various kinds of responses.

res.append

The append method lets us attach response headers to our responses.

For example, if we have the following code:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {  
  res.append('Link', 'http://localhost/', 'http://localhost:3000'])  
  res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly')  
  res.append('Warning', 'Alert')  
  res.send();  
})

app.listen(3000, () => console.log('server started'));

Then when we go to Postman, we should see the same data returned in the Headers tab of the response when we look at the data.

Note that we have to run res.send() to actually send the response.

res.attachment

res.attachment let us add a file to the response. It doesn’t send the response.

For example, we can use it as follows:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))
app.get('/', (req, res) => {  
  res.attachment('../public/foo.txt');  
  res.send();  
})

app.listen(3000, () => console.log('server started'));

Then if we have a foo.txt in the public folder, then the file will be downloaded if we make the request to the route.

Note that again we have res.send() to actually send the response.

res.cookie

res.cookie lets us add a cookie to the response.

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {  
  res.cookie('name', 'foo', { domain: 'repl.it', path: '/', secure: true })  
  res.send();  
})

app.listen(3000, () => console.log('server started'));

Then we send a cookie with name foo to the client. We can check in Postman under the Cookies link in the top right corner.

res.download

res.download sends a file response to the server.

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))
app.get('/', (req, res) => {  
  res.download('./public/foo.txt');  
})
app.listen(3000, () => console.log('server started'));

Then when a request is made to this route, then we’ll get a file downloaded.

res.json

res.json lets us send a JSON response to the client. The parameter can be any JSON type, including object, array, string, Boolean, number, or null.

For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {  
  res.json({ message: 'hi' });  
})

app.listen(3000, () => console.log('server started'));

Then we get:

{"message":"hi"}

as the response.

res.redirect

We can use this to redirect to another URL with the string passed in. For example, if we have:

const express = require('express')  
const app = express()
app.use(express.json())  
app.use(express.urlencoded({ extended: true }))
app.get('/', (req, res) => {  
  res.redirect('http://medium.com'));  
})

app.listen(3000, () => console.log('server started'));

Then we’ll see the content of http://medium.com when we make the request to the route above.

res.status

res.status lets us send a status code response. We can use it with the end , send , or sendFile methods by calling them after calling status .

For example, if we have:

const express = require('express')  
const app = express()app.use(express.json())  
app.use(express.urlencoded({ extended: true }))

app.get('/', (req, res) => {  
  res.status(403).end();  
})
app.listen(3000, () => console.log('server started'));

Then we get a 403 response.

Conclusion

Adding routes is simple with Express. We just have to tell it the URL and method to listen for, and the route handler to handle requests that match them.

We can get query strings and URL parameters with the Request object.

Then we can send a status, text, or file according to our preference with the Response object.

Categories
JavaScript Nodejs

Node.js’ fs Module — Getting File Information

Manipulating files and directories are basic operations for any program. Since Node.js is a server-side platform and can interact with the computer that it’s running on directly, being able to manipulate files is a basic feature.

Fortunately, Node.js has an fs module built into its library. It has many functions that can help with manipulating files and folders. File and directory operations that are supported include basic ones like manipulating and opening files in directories.

Likewise, it can do the same for files. It can do this both synchronously and asynchronously. It has an asynchronous API that has functions that support promises. Also, it can show statistics for a file.

Almost all the file operations that we can think of can be done with the built-in fs module.

In this article, we will use the functions in the fs module to get data about a file located in the fs.Stats object with the fs.stat(), fs.lstat(), and fs.fstat() functions.

To get information about a file, we can use the fs.Stats object, which is returned by the fs.stat(), fs.lstat(), and fs.fstat() functions, and their synchronous counterparts.

They can display numeric data as bigint as it’s passed in as an option key with its value set to true. It has nanosecond-precision properties suffixed with Ns.

The stat function takes a path object which can be a string, Buffer, or a URL object as the first argument.

A second argument is an object that can take the bigint as the key, which is a boolean value. If it’s set to true, then numerical information will be returned as bigInts.

The third argument is a callback function that has the error object for the first parameter and the stats object as the second parameter, which has the information about a file and it’s running when the file information is retrieved.

The stat function runs asynchronously. Its synchronous counterpart is the statSync function, which takes the same first two arguments without the callback function. statSync returns the file information as an object.

lstat is similar to stat, but it doesn’t follow the symbolic link. It takes a path object which can be a string, Buffer, or a URL object as the first argument.

A second argument is an object which can take the bigint as the key, which is a boolean value. If it’s set to true, then numerical information will be returned as bigInts.

The third argument is a callback function that has the error object for the first parameter and the stats object as the second parameter, which has the information about a file and it’s running when the file information is retrieved.

When the path that’s passed in is a symbolic link, then it gives the information about the symbolic link. lstat runs asynchronously, so that the data is retrieved in an indeterminate amount of time.

Its synchronous counterpart, the lstatSync function, takes the same arguments as the lstat function without the callback function and returns the Stat object which has the file information.

The fstat function is similar to the stat function. It takes a path object which can be a string, Buffer, or an URL object as the first argument.

The second argument is an object which can take the bigint as the key, which is a boolean value. If it’s set to true, then numerical information will be returned as bigInts.

The third argument is a callback function that has the error object for the first parameter and the stats object as the second parameter, which has the information about a file and it’s running when the file information is retrieved.

The only difference between stat and fstat is that it takes a file descriptor instead of a path object.

We can get the file descriptor from the callback that’s accepted by the fs.open function and its promise and synchronous counterparts, fsPromises.open, and fs.opensync.

To use the fs.stat function, we can use it like in the following code:

const fs = require("fs");
fs.stat("./files/file.txt", (err, stat) => {  
  if (err) throw err;  
  console.log(stat);  
});

Then, if we run the code above, we get something like the following output:

Stats {  
  dev: 3605029386,  
  mode: 33206,  
  nlink: 1,  
  uid: 0,  
  gid: 0,  
  rdev: 0,  
  blksize: 4096,  
  ino: 22799473115106240,  
  size: 0,  
  blocks: 0,  
  atimeMs: 1572569358035.625,  
  mtimeMs: 1572569358035.625,  
  ctimeMs: 1572569358035.625,  
  birthtimeMs: 1572569358035.625,  
  atime: 2019-11-01T00:49:18.036Z,  
  mtime: 2019-11-01T00:49:18.036Z,  
  ctime: 2019-11-01T00:49:18.036Z,  
  birthtime: 2019-11-01T00:49:18.036Z  
}

As we can see, the Stats object has many properties. The data properties are listed above. It also has a few function properties.

The data properties in the Stats object means the following:

  • dev — The numeric identifier of the device storing the given file. It can be a number or a bigInt.
  • ino — The “inode” number of the file. It’s a number that contains basic information about a file, directory, or other file system object. It can be a number or a bigInt.
  • mode — Bit-field description of the file type and mode. It can be a number or a bigInt.
  • nlink — Number of hard links that exist for the file. It can be a number or a bigInt.
  • uid — The numeric user identifier of the user that owns the file. Applicable to POSIX systems only. It can be a number or a bigInt.
  • gid — The numeric group identifier of the user that owns the file. Applicable to POSIX systems only. It can be a number or a bigInt.
  • rdev — Numeric device identifier of the file if it’s a special file. A file is special if it’s used for I/O. For example, page files and hibernation files are considered special files. It can be a number or a bigInt.
  • size — The size of the file in bytes. It can be a number or a bigInt.
  • blksize — The block size for a file system I/O. It can be a number or a bigInt.
  • blocks — The number of blocks allocated to the file. It can be a number or a bigInt.
  • atimeNs — The timestamp indicating when the file was last accessed in nanoseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • mtimeNs — The timestamp indicating when the file was last modified in nanoseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • ctimeNs — The timestamp indicating when the file was last changed in nanoseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • birthtimeNs — The timestamp indicating when the file was created in nanoseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • atime — The timestamp indicating when the file was last accessed in milliseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • mtime — The timestamp indicating when the file was last modified in milliseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • ctime — The timestamp indicating when the file was last changed in milliseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • birthtime — The timestamp indicating when the file was created in milliseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.

The Stats object also has the following function properties to check for the basic information about a file:

  • isBlockDevice() — This is a function with a boolean return value that returns true if the file is a block device. A block device refers to a file that represents the device that stores files in blocks and also retrieves them as such.
  • isCharacterDevice() — This is a function with a boolean return value that returns true if the file is a character device. A character device refers to a file that represents the device that provided unbuffered, direct access to the hardware device. They don’t have to allow programs to read or write a single character at a time.
  • isDirectory() — This is a function with a boolean return value that returns true if the item is a directory.
  • isFIFO() — This is a function with a boolean return value that returns true if the item is a first-in-first-out pipe. FIFO pipe means that the first bits of a file going into the device will be the same ones that come out when retrieved. It only allows for unidirectional communication.
  • isFile() — This is a function with a boolean return value that returns true if the item is a file.
  • isSocket() — This is a function with a boolean return value that returns true if the item is a socket. A socket is a special file that enables communication between two processes. It can send data and file descriptors across a domain socket. It can do bidirectional communication.
  • isSymbolicLink() — This is a function with a boolean return value that returns true if the item is a symbolic link. A symbolic link is a reference to another file or directory in the form of an absolute or relative path.

To use the synchronous version of the stat function, the statSync function, we can write something like the following code:

const fs = require("fs");
const stat = fs.statSync("./files/file.txt");  
console.log(stat);

The Stat object is returned directly from the statSync function.

To use the fstat function, we have to get the file descriptor first, which we can do with the open function and its variants. For example, if we want to use the open function to get the file descriptor, we can write the following:

const fs = require("fs");
fs.open("./files/file.txt", "r", (err, fd) => {  
  if (err) throw err;  
  fs.fstat(fd, (err, stat) => {  
    if (err) throw err;  
    console.log(stat);  
    fs.close(fd, err => {  
      if (err) throw err;  
    });  
  });  
});

We can use fstat with the promise version of the open function like in the following code:

const fsPromises = require("fs").promises;  
const fs = require("fs");
(async () => {  
  const fdObj = await fsPromises.open("./files/file.txt", "r");  
  fs.fstat(fdObj.fd, (err, stat) => {  
    if (err) throw err;  
    console.log(stat);  
    fs.close(fdObj.fd, err => {  
      if (err) throw err;  
    });  
  });  
})();

The promise version of the open function returns a promise that resolves to an object with the file descriptor inside it. We can use the fd property to get the file descriptor and pass it into the fstat function.

Likewise, with the lstat function, we can call it as in the following code:

const fs = require("fs");
fs.lstat("./files/file.txt", (err, stat) => {  
  if (err) throw err;  
  console.log(stat);  
});

The lstat does almost everything as stat does, except that it gets the data of a symbolic link instead of following it, so we will get output similar to the ones above.

The fs.stat(), fs.lstat(), and fs.fstat() functions are very useful for getting data about files, directories, and symbolic links.

They can get us the Stat object which has lots of information that we can use, including access time, modified time, whether the file is a special file, the device it’s stored on, and many other pieces of information through its value and function properties.

Data can be displayed as numbers or bigInt if they’re numerical and timestamps are available in both milliseconds and nanoseconds for extra precision.

Categories
JavaScript Nodejs

Node.js’s ‘fs’ Module: Writing Files and Directories

Manipulating files and directories are basic operations for any program. Since Node.js is a server-side platform and can interact with the computer that it’s running on directly, the ability to manipulate files is a basic feature.

Fortunately, Node.js has a fs module built into its library. It has many functions that can help with manipulating files and folders. It supports basic file and directory operations, like manipulating and opening files in directories.

It can do the same for files. It can do this both synchronously and asynchronously. It has an asynchronous API with functions that support promises. Also, it can show statistics for a file.

Almost all the file operations that we can think of can be done with the built-in fs module. In this piece, we’ll use the functions in the fs module to write files with the write family of functions to write files.


fs.Write

There are two versions of the write functions, one for writing text to disk and another for writing binary data to disk.

The text version of write function lets us write text onto the disk asynchronously. It takes a few arguments.

The first argument is the file descriptor — a number that identifies the file.

The second argument is a string that’s written to the file. If the value passed in is not a string, it is converted to one.

The third argument is the position in which the file writing starts. If the value passed in isn’t a number, then it starts in the current position.

The fourth argument is a string that has the character encoding of the file to be written, which defaults to be utf8. The last argument is a callback function with three parameters.

The first is the err object which has the error object and it’s not null if there’s an error.

The second parameter is the written parameter, an integer which specifies how many bytes are written to the file system. It’s not necessarily the same as the number of string characters written.

The third parameter is the string parameter that has the string that was written.

On Linux, positional writes don’t work in the append model. On Windows, if the file descriptor is 1, which stands for the standard output, then strings that have non-ASCII characters won’t be rendered properly by default.

To use the write function, we can use the open function to get the file descriptor of the file you want to write to first, then we can write to the file by passing in the file descriptor to the write function. For example, if we want to write to the file with the path ./files/file.txt , we can write something like this:

const fs = require("fs");
fs.open("./files/file.txt", "r+", (err, fd) => {  
  if (err) throw err;  
  fs.write(fd, "abc", 0, "utf8", (err, written, string) => {  
    console.log(err, written, string);  
    fs.close(fd, err => {  
      if (err) throw err;  
    });  
  });  
});

When we run the code above, we should get output that looks something like this:

null 3 abc

In the code above, we first open the file with the open function. We pass in the r+ flag so that we can write to the file. Then we get the file descriptor fd in the callback function that we passed into the open function. With the fd file descriptor, we can pass it into the write function.

In the second argument of the write function we specified that we want to write the string abc to the file. In the third argument, we specified that we want to write it at position 0, the fourth argument specifies that the character encoding of the string should be UTF-8.

The callback in the last argument would get us the result of the write. From there, we know from the output that three bytes and the string ‘abc’ were written to the file.

Other than the r+ flag, there are many other possible system flags, including:

  • 'a' — Opens a file for appending, which means adding data to the existing file. The file is created if it does not exist.
  • 'ax' — Like 'a' but an exception is thrown if the path exists.
  • 'a+' — Open file for reading and appending. The file is created if it doesn’t exist.
  • 'ax+' — Like 'a+' but an exception is thrown if the path exists.
  • 'as' — Opens a file for appending in synchronous mode. The file is created if it does not exist.
  • 'as+' — Opens a file for reading and appending in synchronous mode. The file is created if it does not exist.
  • 'r' — Opens a file for reading. An exception is thrown if the file doesn’t exist.
  • 'r+' — Opens a file for reading and writing. An exception is thrown if the file doesn’t exist.
  • 'rs+' — Opens a file for reading and writing in synchronous mode.
  • 'w' — Opens a file for writing. The file is created (if it does not exist) or overwritten (if it exists).
  • 'wx' — Like 'w' but fails if the path exists.
  • 'w+' — Opens a file for reading and writing. The file is created (if it does not exist) or overwritten (if it exists).
  • 'wx+' — Like 'w+' but an exception is thrown if the path exists.

The binary version of the write function lets us write text onto the disk asynchronously. It takes a few arguments.

The first argument is the file descriptor which is a number that identifies the file.

The second argument is the buffer object which can be of type Butter, TypedArray or DataView.

The third argument is the offset , which determines the part of the buffer to be written.

The fourth argument is the length argument that specifies the number of bytes being written, the last argument is the position which is an integer which describes the position in which the write function will start writing.

The final argument is a callback function — a function that takes the err parameter, which has the error object. If an error occurs, the second is the bytesWritten parameter which gets us the number of bytes written to disk, the third is the buffer object, which has the binary data which was written to disk.

For example, we can use it as in the following code:

const fs = require("fs");
fs.open("./files/binaryFile", "w", (err, fd) => {  
  if (err) throw err;  
  fs.write(fd, new Int8Array(8), 0, 8, 0, (err, bytesWritten, buffer) => {  
    console.log(err, bytesWritten, buffer);  
    fs.close(fd, err => {  
      if (err) throw err;  
    });  
  });  
});

We get the following output if we run it:

null 8 Int8Array [  
  0, 0, 0, 0,  
  0, 0, 0, 0  
]

fs.writeSync

The synchronous version of the write function is the writeSync function. There’s one version for writing binary data and one for writing text data.

The text version of thewriteSync function lets us write text onto the disk asynchronously. It takes a few arguments.

The first argument is the file descriptor which is a number that identifies the file.

The second argument is a string that will be written to the file. If the value passed in is not a string, it will be converted to one.

The third argument is the position in which the file writing starts. If the value passed in isn’t a number, then it starts in the current position.

The fourth argument is a string that has the character encoding of the file be written, which defaults to be utf8. The number of bytes written is returned.

We can use the text version of writeSync function as in the following code:

const fs = require("fs");
const fd = fs.openSync("./files/file.txt", "r+");  
const numBytesWritten = fs.writeSync(fd, "abc", 0, "utf8");  
console.log(numBytesWritten);

We should get 3 in the console.log statement since 3 bytes were written.

The binary data version of the writeSync function takes five arguments. It takes a file descriptor as the first argument. The second argument is the buffer object, which can be a Buffer, TypedArray or DataView object.

The third argument is the offset, which is the integer which specifies the part of the buffer to which the writeSync function will start writing.

The fourth argument is the length argument, which specifies the number of bytes being written.

The last argument is the position, which is an integer that describes the position in which the writeSync function will start writing. The number of bytes written is returned.

We can use the binary version of the writeSync function as in the following code:

const fs = require("fs");
const fd = fs.openSync("./files/binaryFile", "w");  
const numBytesWritten = fs.writeSync(fd, new Int8Array(8), 0, 8, 0);  
console.log(numBytesWritten);

We should get 8 in the console.log statement since 8 bytes were written.


fs.writeFile

We can write files in a less complicated way with the fs.writeFile function. It takes four arguments.

The first argument is the reference to the file, which can be a string path, a Buffer object, a URL object or a file descriptor, which is an integer that identifies the file.

The second argument is the data to be written to the file, which can be a string, a Buffer object, a TypedArray, or a DataView object. The third arguments is for passing in an object with a few options.

There are three things that can be set: the encoding, the mode, and the flag:

  • The encoding specifies the character encoding of the text being written which defaults to utf8 .
  • The mode is an integer which defaults to 0o666. The mode sets the file permission and the sticky bits but only if the file was already created. 0o666 means both readable and writable.
  • The flag option specifies the read or writes options of the file being written. The ones listed above are all valid for this argument. The last argument is the callback function, which is a function that takes an err object which is not null if an error exists.

For example, if we want to write to the file with the path ./files/file.txt with the writeFile function, we can write something like this:

const fs = require("fs");
fs.writeFile(  
  "./files/file.txt",  
  "abc",  
  { encoding: "utf8", mode: 666, flag: "w" },  
  err => console.log(err)  
);

In the first argument, we pass in the string path of the file. Then, in the second argument, we passed in the content to be written. The third argument has the encoding options, file permission of the file being written with the mode option, and the flag to set the mode that the file is open with. If the file is written successfully. The console.log statement above should output null and that you can see the content of the file you wrote onto the disk.

When a file descriptor is passed into the first argument of the writeFile function, the behavior is slightly different from passing in other objects to reference the file. The file is not replaced if a file descriptor is passed into the first argument.

This means that if the writeFile function is called multiple times with the file descriptor, then the item that’s written to the file will be appended to the file instead of overwriting the existing content. So if the first writeFile call passes in the file descriptor 1 and content a and the second writeFile call passes in the file descriptor 1 and content b, then we get ab in the file with file descriptor 1.


fs.writeFileSync

There’s a synchronous version of writeFile called writeFileSync, which takes the same arguments as the writeFile function except without the callback. It returns undefined . We can use it as in the following function:

const fs = require("fs");
fs.writeFileSync("./files/file.txt", "abc", {  
  encoding: "utf8",  
  mode: 666,  
  flag: "w"  
});

After running the code above, you should see the content written to disk in the file with the given path.

Manipulating files and directories are basic operations for any program. Since Node.js is a server-side platform and can interact with the computer that it’s running on directly, being able to manipulate files is a basic feature. Fortunately, Node.js has a fs module built into its library.

It has many functions that can help with manipulating files and folders. In this piece, we used the functions in the fs module to write files with the write family of functions to write files. We used the open and write functions to first open the file to get the file descriptor, then to write the content to the file. It has separate versions to work with text and binary files.

There’s also a synchronous version of the function called writeSync which takes the same arguments without the callbacks. It also has a text and binary version. To make writing to files more convenient, we can use the writeFile and the writeFileSync function, which can write both text and binary files given reference to a file like a path string or a URL object. writeFile is asynchronous and writeFileSync is synchronous.