API with Node.js, Express and Prisma

Configuration

Every project requires an initial setup. The following is a common setup used in Node.js projects, but it can vary depending on the project’s needs.

Initialize the project

Select a working directory for the project. Projects should be relative to this working directory and should be easily transferable to another location.

Next, run the commands to create the project’s working directory and enter it:

mkdir checklist-api
cd checklist-api

It’s possible to perform these operations via the operating system’s visual interface, but standard OS commands will be provided.

Once in the working directory, the first step is to initialize the Node.js project using the npm utility:

npm init -y

W> All these commands should be executed in the project’s working directory.

The above command generates the package.json file, which is the manifesto of the project. Here, you can edit values such as the description, author, and other configurations.

Then, edit the scripts section with commands to run the application in development mode (dev) or in production mode (start):

"scripts": {
  "dev": "node --watch index",
  "start": "node index",
  "test": "echo \"Error: no test specified\" && exit 1"
},

The --watch option is available starting from version 18.11 of Node.js.

Running the script in development mode will allow for the server to automatically restart whenever a change is saved to the project files, incorporating the new changes.

There are other alternatives to the --watch option, such as the nodemon library or pm2 for more robust configurations.

The production script is the industry standard used to run the application on cloud servers.

Next, create the main file for the application:

touch index.js

This is another convention commonly used in Node.js projects and web pages, where the main file is named index.

Modules

To determine which types of modules will be used in the project, add one more option to the package.json file. This is crucial, as it will change the syntax for importing and exporting modules in the project files:

  "license": "ISC",
  "type": "module"
}

There are two types of modules: Common JS (default), which were initially driven by Node.js, and ES Modules which have become the standard in the JavaScript language. The previous configuration "type": "module" sets it to use ES Modules.

Version Control

It’s also possible to use a graphical interface to manage version control with git. Below are the standard commands.

Initialize the git repository:

git init

Create the .gitignore file at the root of the project:

touch .gitignore

Use the following content:

node_modules/
.DS_Store
Thumbs.db

It’s always essential to avoid storing directories generated by Node.js installed libraries (node_modules) as these are rebuilt in each environment and are not part of the source code. Additionally, preliminary display files from directories generated by Windows and Mac OS should be excluded.

Development Tools

Vitest

In every project, it’s important to perform unit or integration tests. One of the libraries in the JavaScript ecosystem used for this purpose is Vitest. It provides a complete suite to execute various types of tests.

Install the Vitest library as a development dependency:

npm install vitest --save-dev

The previous command installs the library in the node_modules directory and lists it as a development dependency in the package.json file under the devDependencies section. Additionally, the command in the testing environment will be vitest:

  "scripts": {
    "dev": "node --watch index",
    "start": "node index",
    "test": "vitest"
  },
  "keywords": [],
  "author": "Gustavo Morales",
  "license": "ISC",
  "type": "module",
  "devDependencies": {
    "vitest": "^0.34.1"
  }
}

The library version can vary depending on when the dependency is installed, as these change over time.

More information:

ESLint

It’s highly recommended to add a tool that helps check the syntax of the files to avoid potential errors as well as standardize the way the code is written. For this, a library called ESLint can be used. Paired with the text editor, it will check the file syntax during development time:

npm install eslint --save-dev

ESLint allows you to select an existing style guide or even create your own guide. It all depends on the agreement of the development team.

The next step is to initialize the ESLint configuration:

npx eslint --init

The npx utility is included in the npm installation, which in turn comes with the Node.js version.

This command initializes a configuration assistant. Answer the configuration assistant’s questions as follows:

? How would you like to use ESLint?
  To check syntax only
  To check syntax and find problems
> To check syntax, find problems, and enforce code style
? What type of modules does your project use?
> JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these
? Which framework does your project use?
  React
  Vue.js
> None of these
? Does your project use TypeScript? > (No) / Yes
? Where does your code run? (Press <space> to select, (a) to toggle all, (i) to invert selection
  Browser
> Node
? How would you like to define a style for your project?
> Use a popular style guide
  Answer questions about your style
? Which style guide do you want to follow?
  Airbnb: https://github.com/airbnb/javascript
  Standard: https://github.com/standard/standard
> Google: https://github.com/google/eslint-config-google
  XO: https://github.com/xojs/eslint-config-xo
? What format do you want your config file to be in?
  JavaScript
  YAML
> JSON
Checking peerDependencies of eslint-config-google@latest
The config that you've selected requires the following dependencies:

eslint-config-google@latest eslint@>=5.16.0
? Would you like to install them now? > No / (Yes)
? Which package manager do you want to use?
> npm
  yarn
  pnpm

Once the assistant has finished, it will generate a file at the root of the project named .eslintrc.json, which contains the whole configuration.

More information:

Visual Studio Code

This section is optional. Any text editor can be used, but this editor comes highly recommended. It has a lot to offer.

The official installer can be found on its official website.

Extensions

The following extensions are suggested for a better development experience with Node.js and JavaScript applications.

It can also be configured so that once a developer opens the project in Visual Studio Code, these are shown as suggestions to install:

mkdir .vscode
touch .vscode/extensions.json

Use the following content:

{
  "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}

Configuration

To add a customized configuration for the project, you first need to create the configuration file:

touch .vscode/settings.json

Inside the settings.json configuration file, place the following information:

{
  "editor.formatOnSave": true,
  "editor.renderWhitespace": "all",
  "editor.renderControlCharacters": true,
  "editor.trimAutoWhitespace": true,
  "editor.tabSize": 2,
  "files.insertFinalNewline": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "prettier.singleQuote": true,
  "prettier.trailingComma": "es5"
}

Some of these settings are:

  • editor.formatOnSave: Ensure the document is always formatted when saving the file.
  • editor.renderControlCharacters: Display all whitespace and special characters.
  • editor.trimAutoWhitespace: Remove all extra whitespace once the document is saved.
  • files.insertFinalNewline: Leave a blank line at the end of each file (required by default by ESLint) and indicate the end-of-line character, mostly for backward compatibility with Windows systems.
  • Set Visual Studio Code’s code formatter as Prettier.
  • Lastly, if you wish to add a comma at the end of some JavaScript structures, you can tell Prettier to do it. This is a good practice when using git, and each new line doesn’t need to add a comma to the previous element.

Format Source Code

Next, install a plugin to specify which configuration Prettier will use:

npm install eslint-config-prettier --save-dev

Finally, modify the .eslint.json file again as follows:

  "extends": ["google", "prettier"],

Debugging with Visual Studio Code

Visual Studio Code offers the option to debug directly within the editor that contains the source code, which is quite useful. To achieve this, you need to create the configuration file:

touch .vscode/launch.json

Use the following information:

{
  "configurations": [
    {
      "name": "Attach by Process ID",
      "processId": "${command:PickProcess}",
      "request": "attach",
      "skipFiles": ["<node_internals>/**"],
      "type": "node"
    }
  ]
}

Whenever a change is made to the configuration in the package.json file, the application should be restarted once again to apply the latest changes.

More information:

Create a Web Server

The goal of this API is to manage a list of tasks per user, and optionally, tasks can be organized into groups. All of this is achieved with data persistence.

Before starting the project, you’ll create a simple web server. Since the application will be a Web REST API that can be consumed using the HTTP protocol, take the Web Server example from the Node.js documentation and place it in the main file:

// index.js

import http from 'http';

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

In the above example code, Node.js uses the http module to create a web server that listens to all requests on the specified IP address and port. As it’s an example, it will respond to any request with the plain text string Hello World.

The code syntax differs slightly due to the decision to use ES Modules instead of Common JS.

To run the application in development mode, use:

npm run dev

When you run the above command, it will occupy the terminal and won’t allow you to execute new commands. Therefore, open a new terminal session to run other commands. In other words, you’ll always have two terminal sessions: one running the application server and another running other commands.

To verify that everything is working as expected, access the following address in your web browser:

http://localhost:3000/

Configuration and Environment Variables

Cross-env

The application won’t always run in a development environment, so it’s important to establish the environment it’s running in. Depending on the environment, you can decide what configuration to use and even connect to different data sources.

For this purpose, you’ll use the environment variable process.env.NODE_ENV, which can be accessed from anywhere in the application using the global process object in Node.js.

Before assigning a value to this environment variable, it’s crucial to ensure compatibility across different operating systems. Each system assigns these variables with a different syntax. Use the library called cross-env. It is installed as a project dependency to ensure it’s always available in the application:

npm install cross-env

Next, modify the scripts section in the package.json file to assign the value of the variable in each script that corresponds to each execution environment:

"scripts": {
  "dev": "cross-env NODE_ENV=development node --watch index",
  "start": "cross-env NODE_ENV=production node index",
  "test": "cross-env NODE_ENV=test jest"
},

More information:

Dotenv

Currently, the configuration values are stored in a JavaScript file and their values are hard-coded in the application’s source code. This is NOT a good practice, as it exposes sensitive data. The idea is to extract this type of information and keep it in separate files for each environment. The development data source is not the same as the production data source.

Additionally, a distinction should be made between application configuration and user configuration. The former consists of values needed for the application to function, such as port number, IP address, and others. The user configuration is more related to application customization, like the number of items per page, and customized user responses.

The recommended library that makes this easy is dotenv. It loads all specified variables from a file into the Node.js environment (process.env) if they aren’t already present in the system. Install the library:

npm install dotenv

Next, create the file where the configuration variables will be stored at the root of the project. Name the file:

touch .env

In the created file, add the first variable:

PORT=3000

It’s crucial NOT to include the .env file in the git repository. This is precisely what you want to avoid - having the variables and their values in the application’s source code. To achieve this, add a new line in the .gitignore file:

node_modules/
.DS_Store
.Thumbs.db
.env

Since this file is not being saved in our repository, create an example copy that can be renamed and used:

cp .env .env.example

Next, centralize all configurations into a single file, regardless of their source. This file will be named config.js and will be placed in a new directory named app:

mkdir app
touch app/config.js

Use the following content:

// app/config.js

import * as dotenv from 'dotenv';

dotenv.config();

export const configuration = {
  server: {
    port: process.env.PORT,
  },
};

The dotenv library will by default look for a file named .env and load the variables and their values into the environment (process.env). This way, the configuration won’t be present in the application’s source code anymore. Additionally, the configuration file (.env) can be replaced in each environment where the application is deployed, or its value can be directly overridden in the operating system’s environment.

Modify the Web Server to always take values from the configuration file:

// index.js

import http from 'http';

import { configuration } from './app/config.js';

const { port } = configuration.server;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World');
});

server.listen(port, () => {
  console.log(`Server running at port: ${port}`);
});

For this application, the hostname is not mandatory, so it’s omitted in the edited file.

More information: