Web App + REST API with Express, PostgreSQL and Nunjucks on Yarn
In this article I'll show you how to create a basic web application in Node from scratch. I'll be using yarn instead of npm. I issue HTTP requests using httpie instead of curl.
Let's start with an empty directory:
cd myapp
$ yarn init
Install express
yarn add express
Inside index.js
, add first (root) route which renders a simple text:
const express = require('express')
const app = express();
app.get('/', (req, res) => {
res.send('Hello World')
})
app.listen(3000, () => {
console.log('listening on 3000')
})
Auto-reloading
Install nodemon to restart the server when there is a change
yarn add nodemon --dev
--dev
as Nodemon is used only for development.
You can run it by hand
./node_modules/.bin/nodemon server.js
or directly with yarn
(as it detects binaries)
yarn run nodemon
or you can put it into package.json
(for later customization)
"scripts": {
"dev": "nodemon index.js"
}
and to be run as
yarn run dev
HTML view
Create index.html
in the root of the project:
<!DOCTYPE html>
<h1>Hello world from a HTML file</h1>
Add path
dependency at the top of index.js
:
const path = require('path')
Add new route, let’s call it /html
app.get('/html', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'))
})
Open localhost:3000/html
in your browser to check if it works.
Request logger
I’ll use morgan as request logger. Let’s install it
yarn add morgan
morgan
is a middleware i.e. a plugin which changes the request or response
object before they get handled by the application itself.
Connect the request logger middleware with the express server
const logger = require('morgan');
app.use(logger('dev’));
Static assets
Let’s specify that public/
directory should be used to serve static assets
(stylesheets, javascript files, images, etc.). This is an example of a built-in
middleware.
app.use(express.static(__dirname + '/public'));
Create the public/
folder in your project’s root directory
mkdir public
Add a simple CSS file. Let’s name it styles.css
body {
color: red
}
Reference the file from your HTML file:
<link rel="stylesheet" href="styles.css" media="screen">
Open localhost:3000/html
in your browser to check if it works.
Form and parsing request body
How to handle form submission in Express ? Let’s start by adding a simple form
to index.html
.
<form action="/calculate" method="POST">
<input type="text" name="name" placeholder="name">
<input type="text" name="amount" placeholder="amount">
<button type="submit">Submit</button>
</form>
Let’s add a POST
route (that matches action
attribute from the form) to
handle the form submission.
app.post('/calculate', (req, res) => {
const { name, amount } = req.body || {};
res.send(`I've got a POST with request body: ${name}, ${amount}`)
})
Express doesn’t handle reading data from the request’s body. We have to use another middleware, called body-parser, to explicitly parse it. Let’s install the middleware
yarn add body-parser
and add it to our Express application.
const bodyParser = require('body-parser')
...
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
urlencoded()
extracts the data submitted via the form (i.e. the content
type is application/x-www-form-urlencoded
) and adds them as body
property to
the request object. the same way json()
extracts the data submitted as JSON
(i.e. the content type is application/json
) and also adds them to the request
object.
app.post('/calculate', (req, res) => {
const { name, amount } = req.body || {};
res.send(`I've got a POST with request body: ${name}, ${amount}`)
})
Submit the form again to check if you see its values.
Templates with Nunjucks
Plain HTML has its limits. Express supports a variety of template engines.
I prefer Nunjucks as it’s just HTML with additional tags. Let’s install
nunjucks
:
yarn add nunjucks
Register it as Express template engine
const nunjucks = require('nunjucks')
nunjucks.configure('views', {
autoescape: true,
express : app
});
Create views/
directory and move our index.html
inside. Adjust h1
slightly
as shown below:
<h1>Hello {{ name }} from a HTML file</h1>
Replace the root route with the following:
app.get('/', (req, res) => {
res.render('index.html', { name: 'Zaiste'} )
})
Open localhost:3000/
in your browser to check if it works.
PostgreSQL
Let’s start by installing pg-promise. It is a library which abstracts away low-level connection management and allows to focus on the business logic. Additionally, it includes a powerful query formatting engine along with the support for automated transactions.
yarn add pg-promise
Create the database
createdb widgets
Create a SQL script that describes widgets
table and enters some data into it:
DROP DATABASE IF EXISTS widgets;
CREATE DATABASE widgets;
\c widgets;
CREATE TABLE widgets (
ID SERIAL PRIMARY KEY,
name VARCHAR,
amount INTEGER
);
INSERT INTO widgets (name, amount)
VALUES ('Widget 1', 33), ('Widget 2', 55), ('Widget 3', 88);
Run the script:
$ psql -f widgets.sql
Add CRUD routes
const widgets = require('./queries/widgets');
...
app.post('/api/widgets', widgets.create)
app.get('/api/widgets', widgets.retrieve)
app.get('/api/widgets/:id', widgets.retrieve)
app.put('/api/widgets/:id', widgets.update)
app.delete('/api/widgets/:id', widgets.remove);
Create queries
folder in the root of your project with widgets.js
inside:
mkdir queries
touch queries/widgets.js
Let’s start with the code that retrieves all widgets from the database. any()
indicates we expect any number of results; it returns a promise:
db.any('select * from widgets')
.then((results) => {
res.status(200)
.json({
status: 'success',
results: results,
message: 'All widgets retrieved'
})
})
.catch((err) => next(err));
It’s very similar to retrieve a single widget
db.one('select * from widgets where id = $1', widgetId)
.then((results) => {
res.status(200)
.json({
status: 'success',
results: results,
message: `Retrieved single widget with ID = ${widgetId}`
});
})
.catch((err) => next(err));
Let’s combine that into a retrieve()
function:
function retrieve(req, res, next) {
if (req.params.id) {
db.one(...)
} else {
db.any(...)
}
}
Create function uses .none()
with some additional pre-processing:
function create(req, res, next) {
const amount = parseInt(req.body.amount);
const name = req.body.name;
db.none('insert into widgets(name, amount)' +
'values(${name}, ${amount})', { name, amount })
.then(() => {
res.status(201)
.json({
status: 'success',
message: 'Widget successfully created'
});
})
.catch((err) => next(err));
}
For update()
function you must always specify all parameters. It is PUT
in
HTTP parlance.
function update(req, res, next) {
const { name, amount } = req.body;
db.none('update widgets set name=$1, amount=$2 where id=$3',
[name, parseInt(amount), parseInt(req.params.id)])
.then(() => {
res.status(200)
.json({
status: 'success',
message: 'Widget successfully updated'
});
})
.catch((err) => next(err));}
Finally, remove()
function:
function remove(req, res, next) {
const widgetId = parseInt(req.params.id);
db.result('delete from widgets where id = $1', widgetId)
.then((results) => {
res.status(200)
.json({
status: 'success',
message: `Widget with ID = ${widgetId} successfully deleted.`
});
})
.catch((err) => next(err));
}
Put all those functions inside queries/widgets.js
. At the top of the file add
the following:
const pg = require('pg-promise')()
const db = pg('postgres://localhost:5432/widgets')
module.exports = { create, retrieve, update, remove }
Let’s test it. First retrieve all elements
http :3000/api/widgets
Retrieve a single widget
http :3000/api/widgets/3
Create a new widget
http :3000/api/widgets name="Widget 4" amount=234 --form
We're parsing request's body using urlencoded
format, that's why I used
--form
flag. Usually we use JSON to communicate with an API. Simply add
bodyParser.json()
to indicate that request's body is in JSON format.
Update an widget (all fields must be specified)
http PUT :3000/api/widgets/4 name="Widget 4" amount=999 --form
Finally, delete a widget
http DELETE :3000/api/widgets/4
These are just basic elements for building a web application which provides a REST-like API to interact with widgets. You can now install Express Generator to automatically recreate most of it, but with a better understanding of the underlaying principles.