NodeJS Express HTTP Server
In this post we’ll look at how to build a simple HTTP server in NodeJS using the express framework.
Server Setup
Required Packages
First step is to install the required npm packages:
1
npm i express body-parser
Basic Setup
Once the packages are installed, we can use the below code to import the packages and setup our first server:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const path = require('path');
const express = require('express');
const bodyParser = require('body-parser');
// Create new Express Server
const app = express();
const server = require('http').Server(app);
// Server Configuration
const PORT = 8080;
// Support JSON encoded POST request bodies
app.use(bodyParser.json());
// Start the Server
server.listen(PORT, () => {
console.log(`[-] Server Listening on Port ${PORT}`);
});
Nice, our server is now setup! But it’s not very useful as it doesn’t actually do anything. Let’s learn how to setup routes/endpoints so we can listen for request and send back data.
Express Basics
Routes
Here is how we create a basic route:
- The first parameter is a string containing the route path (endpoint).
- The second parameter is a callback that will be executed when a request is made to the route.
1
2
3
4
5
6
7
8
9
// GET Example
app.get('/', (req, res) => {
res.send('GET Hello!');
});
// POST Example
app.post('/', (req, res) => {
res.send('POST Hello!');
});
The functions names get
& post
can be changed to any valid HTTP method (OPTIONS, PUT, DELETE, etc) in lowercase.
Sending Back Data
When the callback function is executed, two parameters will be passed in for us to use. These are the req
and res
parameters short for request and response:
req
contains information about the request such as headers and any data in the body that may have been sent.res
is how we send data back to the client, using the thesend
andsendFile
methods.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// The type will automatically be set as HTML if a string is provided.
app.get('/home', (req, res) => {
res.send('<h1>Home</h1>');
});
// The content-type will automatically be set as JSON if an object is provided.
app.get('/json', (req, res) => {
res.send({ json: true });
});
// You can also specify the type manually
app.get('/text', (req, res) => {
res.type('txt');
res.send('<h1>Home</h1>');
});
// Return a file from disk
app.get('/home', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
Response Codes
When data is sent back using the send
or sendFile
methods a response code of 200 is used. We can specify a custom code using the status
and sendStatus
methods:
- If you want to change the status code AND send data back in the body, use the
status
method. - If you only want to send a status code with no body, use the
sendStatus
method.
1
2
3
4
5
6
7
8
9
app.get('/json', (req, res) => {
// Send custom HTTP Status code
res.status(406).send({ json: true });
});
app.get('/admin', (req, res) => {
// Send custom HTTP Status code
res.sendStatus(404);
});
Route Parameters
We use Route Parameters as a placeholder variable for data that will be supplied by the user:
- If the client calls the API
/user/53
, we want to be able to access the value53
. - We can do this by specifying a Route Parameter like this
/user/:userId
. - Route parameters are accessible using
req.params
. This returns an object of all the route parameters. - Using
req.params.userId
will return53
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Route with a Route Parameter
app.get('/user/:userId', (req, res) => {
// For the URI: /user/53
console.log(req.params); // { userId: '53' };
console.log(req.params.userId) // '53'
res.sendStatus(200);
});
// Multiple Route Parameters
app.get('/light/:lightbulbId/:action', (req, res) => {
// For the URI: /light/17/off
console.log(req.params); // { lightbulbId: '17', action: 'off' };
console.log(`Light ${req.params.lightbulbId} will be turned ${req.params.action}`) // 'Light 17 will be turned off'
res.sendStatus(200);
});
Query Parameters
Query Parameters can be supplied by the client can be accessed using req.query
. An object containing all the query parameters will be returned:
1
2
3
4
5
6
7
8
// Query Parameters Example
app.get('/search', (req, res) => {
// For the URI: /search?firstName=John&lastName=Smith
console.log(req.query) // { firstName: 'John', lastName: 'Smith' }
// Send custom HTTP Status code
res.sendStatus(200);
});
Cookies
We can set a cookie in the response by using the cookie
method:
- The first parameters is the cookie name
MyCookie
. - The second is the cookie value
abcdefghijklmnopqrstuvwxyz
. - The third is an object of cookie attributes.
After using the cookie
method, either send
or sendStatus
must be called as well to return the result.
1
2
3
4
5
6
7
8
9
10
app.get('/home', (req, res) => {
res.cookie('MyCookie', 'abcdefghijklmnopqrstuvwxyz', {
maxAge: 3600000, // Time in milliseconds: 3600000 = 1 Hour
domain: 'localhost',
path: '/',
secure: false,
httpOnly: true,
sameSite: 'strict'
}).send({ success: true });
});
Multiple cookies can be set by calling the cookie
method multiple times.
JSON Body
If the client has sent us data in JSON format, here is how we can access that data from the request:
req.body
will return the JSON data in the request body in the form of an object.- NOTE 1: if
req.body
isundefined
, ensure that you haveapp.use(bodyParser.json());
in your code. - NOTE 2: if
req.body
is an empty object, ensure that theContent-Type
in the request header isapplication/json
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Login API
app.post('/login', (req, res) => {
console.log(req.body); // { username: 'John123', password: 'Password_ThatShouldNotBeGuessed641'}
console.log(req.body.username); // 'John123'
console.log(req.body.password); // 'Password_ThatShouldNotBeGuessed641'
let loginSuccessful = false;
if (req.body && req.body.username && req.body.password) {
// Insert Authentication logic here
loginSuccessful = checkLogin(req.body.username, req.body.password)
}
if (loginSuccessful) {
// Login Successful, set a Cookie in the response
res.cookie('auth-token', 'kln450x02sl5gusnh3mpu0s7ca2jfsaf8h', {
maxAge: 3600000, // Time in milliseconds: 3600000 = 1 Hour
httpOnly: true,
sameSite: 'lax'
}).send({ success: true });
} else {
// Login Failed
res.send({ success: false });
}
});
More Advanced Stuff
Custom Headers
Custom response headers can be added using the header
method to add controls such as CORS, HSTS, CSP, etc:
- The first parameter is the header name.
- The second parameter is the header value.
1
2
3
4
5
app.get('/home', (req, res) => {
res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:8080');
res.sendFile(path.join(__dirname, 'index.html'));
});
Next Route
A common situation that can occur is when you need to do the same thing in multiple routes. An example of this might be adding a header, or checking if a user is authenticated.
Adding these headers/checks in every route is bad practice and can lead to security issues if you forget to add it in one of routes.
Luckily there is a better way:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Route 1
app.use((req, res, next) => {
res.header('Custom-Header', 'NodeJS Server');
next();
})
// Route 2
app.get('/home', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
// Route 3
app.use((req, res, next) => {
// Insert logic to check if the user's cookie is valid
let authed = true;
if (authed) {
next();
} else {
res.sendStatus(404);
}
})
// Route 4
app.get('/authed/home', (req, res) => {
res.sendFile(path.join(__dirname, 'home.html'));
});
The use
function matches every route, so putting a app.use
before all other routes will ensure it gets called first.
When we request /home
the following will happen:
- Route 1 is be matched, custom response header is be added, and the
next
function is called. - The
next
function means the route is finished doing what it needs to, but it doesn’t want to send a response. Express will continue checking for the next route that matches. - Route 2 is matched and returns
index.html
, completing the request.
When we request /authed/home
the following will happen:
- Route 1 is be matched, custom response header is be added, and the
next
function is called. - Route 2 is NOT matched.
- Route 3 is matched. This route checks to see if the user authorized to view the subsequent pages. If yes, then
next
is called. If not, then a 404 is sent to the client. - If the user is authorised, then Route 4 is matched and returns
home.html
, completing the request.