Node and ExpressJS
Learning objectives
- You see how a JSON API could be implemented with Node and ExpressJS.
Note that this example is also a bit outdated. For example, the
body-parser
is no longer needed as it is included in ExpressJS.
For working with the following example on your computer, you need to have a recent version of NodeJS. We recommend using nvm for managing Node versions. In Node projects, dependencies -- called packages -- are retrieved using npm (node package manager). Node package manager also helps with project management related tasks such as creating a new project, starting the project, and so on.
We start with an empty folder and create a new project with npm init -y
.
tree --dirsfirst
.
0 directories, 0 files
npm init -y
Wrote to /path-to-file/package.json:
{
"name": "nodejs-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
The command npm init -y
creates a new project without asking for project details. The command creates a package.json
file which is used to manage the project dependencies and project tasks. Now, our folder has one file.
tree --dirsfirst
.
└── package.json
0 directories, 1 file
ExpressJS is a web framework for building NodeJS applications. Let's add ExpressJS to our project using npm
. New packages are added using the command npm install package --save
, where package refers to the package that we wish to add.
npm install express --save
// ...
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN nodejs-example@1.0.0 No description
npm WARN nodejs-example@1.0.0 No repository field.
+ express@(version)
added 50 packages from 37 contributors and audited 50 packages in 2.355s
found 0 vulnerabilities
Now, when we look at the package.json
file, we notice that ExpressJS has been added to the project as a dependency. In addition, a folder called node_modules
has been added to the folder -- project dependencies are loaded to the node_modules
folder.
tree --dirsfirst
.
├── node_modules
│ └── // lots of stuff...
├── package.json
└── package-lock.json
71 directories, 316 files
Controller and request parameters
Let's start by creating a file called app.js
. In the file, we load express using the command require
. Then, we create an express application, and define a function called hello
that is used to handle a request. The function takes a request and a response as a parameter, and sends the text Hello world!
as a response to the request. Then, we specify that GET requests made to the root path of the application are handled by the function hello
. Finally, we start listening for connections on the port 7777
.
const express = require('express');
const app = express();
const hello = (req, res) => {
res.send('Hello world!');
}
app.get('/', hello);
app.listen(7777);
Now, we can launch the application using the node
command. The above application is run using the command node app.js
.
node app.js
When the application is running, we can make requests to the server. When we make a request to the address http://localhost:7777
, we see the following response.
curl http://localhost:7777
Hello world!%
Stopping the application is done using ctrl+C.
Node applications often use nodemon or something similar for watching changes to the source code and restarting the application when needed. Let's add nodemon to the project. The flag --save-dev
indicates that the package is used for development.
npm install nodemon --save-dev
// ...
+ nodemon@(version)
added 117 packages from 53 contributors and audited 168 packages in 5.797s
10 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Now, we modify the package.json
adding a script to it. Scripts are placed within "scripts"
. The new line "start": "nodemon app.js"
in "scripts"
allows us to start the application using nodemon
with the command npm start
.
{
"name": "nodejs-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "nodemon app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1"
},
"devDependencies": {
"nodemon": "^2.0.6"
}
}
Now, when we start the application with the command npm start
, nodemon starts to watch for changes in files within the current directory.
npm start
> nodejs-example@1.0.0 start /path-to-file
> nodemon app.js
[nodemon] 2.0.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node app.js`
If we change the file app.js
and save it while the application is running, we notice that the application is restarted.
// ...
[nodemon] restarting due to changes...
[nodemon] starting `node app.js`
Next, we add functionality for posting data to the server.
To handle data that is posted to the server, we use a package called body-parser. The package is already retrieved when we installed ExpressJS, so we need to only add it to our application -- adding it to our application is done as follows. We first load body-parser
using require
, and then add it to the ExpressJS application using the use
-method. Bodyparser is a middleware, and the use
method adds the middleware to the project -- this is similar to what we have seen with Hono.
const express = require('express');
const bodyParser = require('body-parser')
const app = express();
app.use(bodyParser.urlencoded())
// ...
Now, requests made to the server have a variable called body
in the request. Let's adjust the application so that, for POST requests made to the root path of the server, the server logs the request body to the console and responds with the status code 200
. At this point, the application looks as follows.
const express = require('express');
const bodyParser = require('body-parser')
const app = express();
app.use(bodyParser.urlencoded({extended: false}))
const hello = (req, res) => {
res.send('Hello world!');
}
const setMessage = (req, res) => {
console.log(req.body);
res.sendStatus(200);
}
app.get('/', hello);
app.post('/', setMessage);
app.listen(7777);
Now, when the server is running and we make a request to the server, we see the output in the server console.
curl --verbose --request POST --data "message=NodeJS and ExpressJS" http://localhost:7777
// ...
< HTTP/1.1 200 OK
// ...
// ...
[nodemon] starting `node app.js`
[Object: null prototype] { message: 'NodeJS and ExpressJS' }
We can access the value of the variable message
sent in the request body using req.body.message
. In the following example, the application has a variable called message
, which is used as a part of the response to GET requests made to the root path of the server. The variable value can be changed by making a POST request to the server. In addition to setting the value of the variable message
, POST requests made to the root path of the application are sent a redirect response.
const express = require('express');
const bodyParser = require('body-parser')
const app = express();
app.use(bodyParser.urlencoded({extended: false}))
let message = 'world';
const hello = (req, res) => {
res.send(`Hello ${message}!`);
}
const setMessage = (req, res) => {
message = req.body.message;
res.redirect('/');
}
app.get('/', hello);
app.post('/', setMessage);
app.listen(7777);
Now, when testing the server, we see that the message sent from the server can be changed.
curl http://localhost:7777
Hello world!%
curl --verbose --request POST --data "message=NodeJS and ExpressJS" http://localhost:7777
// ...
< HTTP/1.1 302 Found
// ...
Found. Redirecting to /%
curl http://localhost:7777
Hello NodeJS and ExpressJS!%
A simple API
Let's continue by creating a simple API -- we do the same API as in the previous examples. The API is used for adding and reading songs and their ratings and it uses a database. The database table is the one that we've used previously.
CREATE TABLE songs (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
rating INTEGER NOT NULL
);
To access the database, we use node-postgres, which is a collection of packages used for working with the PostgreSQL database.
Note that this example would also work with Postgres.js which we have used in the course. The example here is a bit outdated.
Let's add node-postgres
to our project.
npm install node-postgres --save
// ...
+ node-postgres@(version)
added 1 package from 1 contributor and audited 169 packages in 2.895s
10 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
In addition, we need pg, which is a PostgreSQL client.
npm install pg --save
// ...
+ pg@(version)
added 19 packages from 11 contributors and audited 188 packages in 2.66s
10 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Now, once we have node-postgres
and pg
installed, we can connect to the database. The basic configuration for importing the needed libraries and creating a connection pool is as follows. We could also inject the parameters as environment variables.
const { Pool } = require("pg");
const pool = new Pool({
host: "my-host-name-possibly-at.elephantsql.com",
user: "my-username",
password: "my-password",
database: "my-database-possibly-same-as-username",
port: 5432,
});
Retrieving a connection from the connection pool is done using const connection = await pool.connect()
and releasing the connection is done using connection.release()
. Queries are done using a method called query
, i.e. await connection.query('sql-statement')
, which returns a query result. The query results are in the variable rows
of the results
-- the rows
object is a JavaScript object.
In the following example, GET requests made to the path /songs
are handled by a function that connects to the database, retrieves all songs from the database, and returns the songs as a response to the request.
const express = require("express");
const bodyParser = require("body-parser");
const { Pool } = require("pg");
const pool = new Pool({
host: "my-host-name-possibly-at.elephantsql.com",
user: "my-username",
password: "my-password",
database: "my-database-possibly-same-as-username",
port: 5432,
});
// ...
const getSongs = async (req, res) => {
const connection = await pool.connect();
const result = await connection.query("SELECT * FROM songs");
connection.release();
res.send(result.rows);
};
// ...
app.get("/songs", getSongs);
app.listen(7777);
Now, when we make a GET request to the path /songs
, we receive a JSON document containing the songs from the database as a response.
curl http://localhost:7777/songs
[{"id":1,"name":"Songiti song song","rating":5},{"id":2,"name":"Happy song","rating":5},{"id":3,"name":"Happier song","rating":2}]%
Next, we implement the functionality for posting content in JSON format to the server. The body-parser
library that we use provides the functionality for processing JSON data in addition to processing form-like data that we have used previously. Adding the middleware for processing JSON requests is done with the use
method of application -- to start processing JSON data, we pass it bodyParser.json()
which is a middleware for processing JSON data.
const express = require('express');
const bodyParser = require('body-parser')
const app = express();
app.use(bodyParser.urlencoded({extended: false}))
app.use(bodyParser.json());
// ...
Now, if a POST request that contains a JSON document is sent to the server, the JSON data is converted to a JavaScript object that is then set as the value of the body
variable in the request. In the following example, we have created a function addSong
that will be used for adding songs. Now, the function logs the request body to the server console. POST requests to the path /songs
are handled by the function addSong
.
// ...
const addSong = async(req, res) => {
console.log(req.body);
res.sendStatus(200);
}
// ...
app.post('/songs', addSong);
// ...
When the server is running, we can send JSON data to the server, and the server knows how to process it. In the example below, we send the data to the server using curl -- the data is then logged into the server console.
curl --verbose --header "Content-Type: application/json" --request POST --data "{\"name\":\"Another song\",\"rating\":3}" http://localhost:7777/songs
// ...
< HTTP/1.1 200 OK
// ...
[nodemon] starting `node app.js`
{ name: 'Another song', rating: 3 }
Let's adjust the addSong
function so that it adds the song to the database. Similar to retrieving data from the database, we need to retrieve a connection from the connection pool, make a query to the database, and release the connection. In this case, as we wish to add data to the database, we use an INSERT
query.
Regarding the query parameters, node-postgres
assumes that the parameters are passed to the query function in a list and identified in the query string through indexes where $1
in the query corresponds to the first item in the list, $2
in the query corresponds to the second item in the list, and so on. In the example below, the function addSong
adds a new song to the database using the data send in the query.
// ...
const addSong = async (req, res) => {
const connection = await pool.connect();
await connection.query("INSERT INTO songs (name, rating) VALUES ($1, $2)", [
req.body.name,
req.body.rating,
]);
connection.release();
res.sendStatus(200);
};
// ...
// ...
app.post("/songs", addSong);
// ...
Now, when we make a request to the server, we observe that we can add songs to the database.
curl --verbose --header "Content-Type: application/json" --request POST --data "{\"name\":\"Another song\",\"rating\":3}" http://localhost:7777/songs
// ...
< HTTP/1.1 200 OK
// ...
curl http://localhost:7777/songs
[{"id":1,"name":"Songiti song song","rating":5},{"id":2,"name":"Happy song","rating":5},{"id":3,"name":"Happier song","rating":2},{"id":4,"name":"Another song","rating":3}]%
The full source code for the project that we implemented is as follows.
const express = require("express");
const bodyParser = require("body-parser");
const { Pool } = require("pg");
const pool = new Pool({
host: "my-host-name-possibly-at.elephantsql.com",
user: "my-username",
password: "my-password",
database: "my-database-possibly-same-as-username",
port: 5432,
});
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
let message = "world";
const hello = (req, res) => {
res.send(`Hello ${message}!`);
};
const setMessage = (req, res) => {
message = req.body.message;
res.redirect("/");
};
const getSongs = async (req, res) => {
const connection = await pool.connect();
const result = await connection.query("SELECT * FROM songs");
connection.release();
res.send(result.rows);
};
const addSong = async (req, res) => {
const connection = await pool.connect();
await connection.query("INSERT INTO songs (name, rating) VALUES ($1, $2)", [
req.body.name,
req.body.rating,
]);
connection.release();
res.sendStatus(200);
};
app.get("/", hello);
app.post("/", setMessage);
app.get("/songs", getSongs);
app.post("/songs", addSong);
app.listen(7777);
Brief summary
When working with NodeJS and ExpressJS, we use npm
for handling packages. Packages installed to the project are added to a project-specific node_modules
folder. Similar to working with Hono and Deno, when working with ExpressJS, we map the request methods and paths to functions using explicit functions. This is somewhat in contrast to the approach taken in e.g. Spring Boot and FastAPI, where annotations (Java) and decorators (Python) are used to add information to the functions and methods that will then be interpreted during compile or runtime by the web framework.
In the above example, we used SQL queries directly, and did not use an ORM. This does not mean that we couldn't have used one though -- in addition to Prisma, one popular ORM for NodeJS is Sequelize.