Designing a RESTful API with Node and Postgres
In this tutorial we’ll create a RESTful web service with JavaScript, Node, Express, Postgres, and pg-promise.
Our app will include the following endpoints:
URL | HTTP Verb | Action |
---|---|---|
/api/puppies | GET | Return ALL puppies |
/api/puppies/:id | GET | Return a SINGLE puppy |
/api/puppies | POST | Add a puppy |
/api/puppies/:id | PUT | Update a puppy |
/api/puppies/:id | DELETE | Delete a puppy |
This tutorial uses the following tools and technologies - Node.js v4.x, express-generator v4.x, pg-promise v5.x, PostgreSQL v9.4, and Bluebird v3.x
Contents
Project setup
Install the Express Generator (if necessary):
$ npm install express-generator@4 -g
Create a new project and install the required dependencies:
$ express node-postgres-promises
$ cd node-postgres-promises
$ npm install
Test!
$ npm start
Navigate to http://localhost:3000 in your browser, and you should see the familiar “Welcome to Express” text. Kill the server when done. Now let’s set up the Postgres bindings via pg-promise…
Install pg-promise
$ npm install pg-promise@5 --save
Why pg-promise
instead of pg
? Put simply, pg-promise abstracts away much of the difficult, low-level connection management, allowing you to focus on the business logic. Further, the library includes a powerful query formatting engine and support for automated transactions.
Finally, create a new file in the project root called queries.js:
var promise = require('bluebird');
var options = {
// Initialization Options
promiseLib: promise
};
var pgp = require('pg-promise')(options);
var connectionString = 'postgres://localhost:5432/puppies';
var db = pgp(connectionString);
// add query functions
module.exports = {
getAllPuppies: getAllPuppies,
getSinglePuppy: getSinglePuppy,
createPuppy: createPuppy,
updatePuppy: updatePuppy,
removePuppy: removePuppy
};
Here, we created an instance of pg-promise
and assigned it to a variable, pgp
.
Did you notice that we passed an object, options
, during the initialization process? This is required, even if you do not pass any properties/initialization options to the object. In this case, we overrode pg-promise’s default promise library - ES6 Promises - with Bluebird by setting the promiseLib
property in the options
object.
Why Bluebird? It’s loaded with features and reputed to be faster than ES6 Promises.
Don’t forget to install Bluebird:
$ npm install bluebird@3 --save
Next, we defined a connection string, and then passed it to the pg-promise instance to create a global connection instance.
Done!
Postgres setup
Create a new file also in the root called puppies.sql and then add the following code:
DROP DATABASE IF EXISTS puppies;
CREATE DATABASE puppies;
\c puppies;
CREATE TABLE pups (
ID SERIAL PRIMARY KEY,
name VARCHAR,
breed VARCHAR,
age INTEGER,
sex VARCHAR
);
INSERT INTO pups (name, breed, age, sex)
VALUES ('Tyler', 'Retrieved', 3, 'M');
Run the file to create the database, apply the schema, and add one row to the newly created database:
$ psql -f puppies.sql
DROP DATABASE
CREATE DATABASE
CREATE TABLE
INSERT 0 1
Routes
Now we can set up the route handlers in index.js:
var express = require('express');
var router = express.Router();
var db = require('../queries');
router.get('/api/puppies', db.getAllPuppies);
router.get('/api/puppies/:id', db.getSinglePuppy);
router.post('/api/puppies', db.createPuppy);
router.put('/api/puppies/:id', db.updatePuppy);
router.delete('/api/puppies/:id', db.removePuppy);
module.exports = router;
Queries
Next, let’s add the SQL queries to the queries.js file…
GET All Puppies
function getAllPuppies(req, res, next) {
db.any('select * from pups')
.then(function (data) {
res.status(200)
.json({
status: 'success',
data: data,
message: 'Retrieved ALL puppies'
});
})
.catch(function (err) {
return next(err);
});
}
In the above code, we utilized the any
Query Result Mask to query the database, which returns a promise object. This method is used to indicate that we are expecting any number of results back. Success and failures are then handled by .then()
and .catch()
.
Besides, any
, you can use the following Query Result Masks (just to name a few):
one
- a single row is expectedmany
- one or more rows are expectednone
- no rows are expectedresult
- passes the original object when resolved (we’ll look at an example of this shortly)
Test the request out in the browser - http://localhost:3000/api/puppies:
{
"status": "success",
"data": [
{
"id": 1,
"name": "Tyler",
"breed": "Shih-tzu",
"age": 3,
"sex": "M"
}
],
"message": "Retrieved ALL puppies"
}
GET Single Puppy
function getSinglePuppy(req, res, next) {
var pupID = parseInt(req.params.id);
db.one('select * from pups where id = $1', pupID)
.then(function (data) {
res.status(200)
.json({
status: 'success',
data: data,
message: 'Retrieved ONE puppy'
});
})
.catch(function (err) {
return next(err);
});
}
Again, test in the browser: http://localhost:3000/api/puppies/1
{
"status": "success",
"data": {
"id": 1,
"name": "Tyler",
"breed": "Shih-tzu",
"age": 3,
"sex": "M"
},
"message": "Retrieved ONE puppy"
}
POST
function createPuppy(req, res, next) {
req.body.age = parseInt(req.body.age);
db.none('insert into pups(name, breed, age, sex)' +
'values(${name}, ${breed}, ${age}, ${sex})',
req.body)
.then(function () {
res.status(200)
.json({
status: 'success',
message: 'Inserted one puppy'
});
})
.catch(function (err) {
return next(err);
});
}
Test with curl in a new terminal window:
$ curl --data "name=Whisky&breed=annoying&age=3&sex=f" \
http://127.0.0.1:3000/api/puppies
You should see:
{
"status": "success",
"message": "Inserted one puppy"
}
Double check the GET ALL route in your browser to ensure that the new puppy is now part of the collection.
PUT
function updatePuppy(req, res, next) {
db.none('update pups set name=$1, breed=$2, age=$3, sex=$4 where id=$5',
[req.body.name, req.body.breed, parseInt(req.body.age),
req.body.sex, parseInt(req.params.id)])
.then(function () {
res.status(200)
.json({
status: 'success',
message: 'Updated puppy'
});
})
.catch(function (err) {
return next(err);
});
}
Test!
$ curl -X PUT --data "name=Hunter&breed=annoying&age=33&sex=m" \
http://127.0.0.1:3000/api/puppies/1
Delete
function removePuppy(req, res, next) {
var pupID = parseInt(req.params.id);
db.result('delete from pups where id = $1', pupID)
.then(function (result) {
/* jshint ignore:start */
res.status(200)
.json({
status: 'success',
message: `Removed ${result.rowCount} puppy`
});
/* jshint ignore:end */
})
.catch(function (err) {
return next(err);
});
}
So, we used the result
Query Result Mask, in order to get the number of records affected by the query.
$ curl -X DELETE http://127.0.0.1:3000/api/puppies/1
Result:
{
"status": "success",
"message": "Removed 1 puppy"
}
Error Handling
Update the error handlers in app.js to serve up JSON:
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function(err, req, res, next) {
res.status( err.code || 500 )
.json({
status: 'error',
message: err
});
});
}
// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
res.status(err.status || 500)
.json({
status: 'error',
message: err.message
});
});
Conclusion
We now have a basic RESTful API built with Node, Express, and pg-promise. Be sure to comment below if you have any questions.
Grab the code from the repo.
NOTE: Check out pg-promise-demo for a more comprehensive example of setting up your database layer.