In this post I will show you how to build a basic RESTful API using Express, a popular NodeJS framework. I will use MongoDB for storage with Mongoose ODM. The application will be written in CoffeeScript and it will be compatible with Heroku.
As we are not concerned about the data, our API will allow to access abstract (and mysterious) widgets using four CRUD operations: create, retrieve, update, delete.
Before We Start
Make sure you have NodeJS installed and MongoDB installed and running. You can check one of my previous articles for details about installing and managing different versions of NodeJS in a convenient way. For MongoDB consider official installation guide.
Minimal Viable Example
Express comes with a script to generate an application scaffold. In this tutorial, however, we will build it from scratch in order to get the whole picture and fully understand what is going on.
We start off by placing two files in a newly created directory:
app.coffee
the core of our applicationpackage.json
which defines application dependencies
express = require 'express'
app = express()
app.configure ->
app.set "port", process.env.PORT or 4000
app.get '/', (req, res) ->
res.send 'Hello, Zaiste!'
app.listen app.get('port'), ->
console.log "Listening on port #{app.get('port')}"
{
"name": "widget-factory-api",
"description": "API for widgets.",
"version": "0.0.1",
"private": true,
"dependencies": {
"coffee-script": "latest",
"express": "3.x"
}
}
Let's run it with coffee app.coffee
and test if it really works:
λ coffee app.coffee &
Listening on port 4000
λ curl localhost:4000
Hello, Zaiste!
For convenience we can also use npm
to launch the server by adding scripts
section to package.json
.
{
...
"scripts": {
"start": "coffee -w app.coffee"
}
}
From now on, we can just npm start
to start the server. Additional -w
parameter means the application is run in a watch mode, i.e. it is reloaded once the source code changes.
Persistance Layer
NodeJS provides many ways to integrate with MongoDB, Mongoosejs is one of them. It is an open-source, object data modeling (ODM) library. It is built on top of NodeJS Native Drive.
The native driver is a more basic, lower level way to integrate with MongoDB while ODM puts in an additional layer of abstraction which helps to « manage » data access. Some people like such approach praising increased development speed, other complain about unnecessary complexity. As always, the truth is somewhere in the middle, it is difficult to generalise and often it depends.
MongoDB has a flexible schema. It means that inside the same collection we can have documents with different set of fields. Mongoose enforces a fixed structure on these flexible collections with schema definitions.
Let's add Mongoose to package.json
followed by npm install
.
{
...
"dependencies": {
"mongoose": "latest"
}
}
Object Modeling
We are ready to define a schema for a Widget
. Each document in a collection is supposed to have four fields: a name, a description, an amount and a creation timestamp.
Let's create a model
directory along with widget.coffee
file inside:
mongoose = require 'mongoose'
Widget = new mongoose.Schema(
name: { type: String, trim: true }
desc: String
amount: { type: Number, min: 0 }
created_at: { type: Date, default: Date.now }
)
mongoose.model "Widget", Widget
Now, inside our app.coffee
, we can require Mongoose library, connect to the database and make the schema available.
mongoose = require 'mongoose'
app.configure ->
...
app.set 'storage-uri',
process.env.MONGOHQ_URL or
process.env.MONGOLAB_URI or
'mongodb://localhost/widgets'
mongoose.connect app.get('storage-uri'), { db: { safe: true }}, (err) ->
console.log "Mongoose - connection error: " + err if err?
console.log "Mongoose - connection OK"
require './model/widget'
The application is also prepared to handle MongoDB providers available on Heroku: MongoHQ and MongoLab. By default, MongoDB favours speed over data safety; here, we explicitly override this setting.
Time to REST
Our API will be RESTful: we operate on a resource called Widget
using four operations
in order to implement CRUD functionality (create, retrieve, update, delete); we interact with our resource by defining two URLs: /widgets
and /widgets/:id
(:id
denotes a variable part of an URL - a resource unique identifier).
First, we include bodyParser
middleware that parses request body so the parameters passed in become available as req.body
.
app.configuration ->
...
app.use express.bodyParser()
In app.coffee
, we specify all necessary routes with their respective operations.
app.post '/widgets', widgets.create
app.get '/widgets', widgets.retrieve
app.get '/widgets/:id', widgets.retrieve
app.put '/widgets/:id', widgets.update
app.delete '/widgets/:id', widgets.delete
We will put implementations of these operations inside controller
folder with each resource having one file; in our case it would be controller/widgets.coffee
.
Let's start with create
operation triggered by a POST
request to /widgets
. Data necessary to create a new resource is supposed to be passed in as request body in JSON format. This way we can simply place it directly as a parameter for Mongoose. Additionally, fields not defined in the schema will be omitted.
exports.create = (req, res) ->
Resource = mongoose.model('Widget')
fields = req.body
r = new Resource(fields)
r.save (err, resource) ->
res.send(500, { error: err }) if err?
res.send(resource)
retrieve
operation checks if there is an ID parameter within URL. If that is the case, it tries to retrieve a single resource with that id, otherwise it returns all resources from the collection.
exports.retrieve = (req, res) ->
Resource = mongoose.model('Widget')
if req.params.id?
Resource.findById req.params.id, (err, resource) ->
res.send(500, { error: err }) if err?
res.send(resource) if resource?
res.send(404)
else
Resource.find {}, (err, coll) ->
res.send(coll)
We update
our resource with PUT
request. The resource ID is passed as URL parameter while the request body (in JSON format) carries information about fields and their values that need to be changed.
exports.update = (req, res) ->
Resource = mongoose.model('Widget')
fields = req.body
Resource.findByIdAndUpdate req.params.id, { $set: fields }, (err, resource) ->
res.send(500, { error: err }) if err?
res.send(resource) if resource?
res.send(404)
Finally, there is a delete
operation that removes a resource with a given ID, specified as URL parameter.
exports.delete = (req, res) ->
Resource = mongoose.model('Widget')
Resource.findByIdAndRemove req.params.id, (err, resource) ->
res.send(500, { error: err }) if err?
res.send(200) if resource?
res.send(404)
Our implementation has a basic error handling mechanism, plus it returns a 500 response when the requested resource is not found. Still, there is no data validation nor security layer implemented.
Testing Routes
All pieces in place, we can now test our application. Let's start by creating first widget:
λ curl -X POST -H 'Content-Type: application/json' \
-d '{ "name": " Widget 1 ", "desc": "This is widget 1", "amount": "10" }' \
localhost:4000/widgets
{
"__v": 0,
"name": "Widget 1",
"desc": "This is widget 1",
"amount": "10",
"_id": "50d8d67f228552594b000001",
"created_at": "2012-11-04T21:26:07.333Z"
}
Let's get all widgets, only one or one that doesn't exist:
λ curl localhost:4000/widgets
[
{
"name": "Widget 1",
"desc": "This is widget 1",
"amount": "10",
"_id": "50d8d67f228552594b000001",
"__v": 0,
"created_at": "2012-11-04T21:26:07.333Z"
}
]
λ curl localhost:4000/widgets/50d8d67f228552594b000001
{
"name": "Widget 1",
"desc": "This is widget 1",
"amount": "10",
"_id": "50d8d67f228552594b000001",
"__v": 0,
"created_at": "2012-11-04T21:26:07.333Z"
}
λ curl localhost:4000/widgets/50d8d67f228552594b000011
Not found
A request to update a widget is similar to the one that creates it:
λ curl -X PUT -H 'Content-Type: application/json' -d '{ "name": "Widget 999" }' \
localhost:4000/widgets/50d8d67f228552594b000001
{
"name": "Widget 999",
"desc": "This is widget 1",
"amount": "10",
"_id": "50d8d67f228552594b000001",
"__v": 0,
"created_at": "2012-11-04T21:26:07.333Z"
}
Finally, let's delete our widget.
λ curl -X DELETE localhost:4000/widgets/50d267b593b4090000000001
OK
λ curl localhost:4000/widgets/50d8d67f228552594b000001
Not found
λ curl localhost:4000/widgets
[]
Summary
We built a basic RESTful API in NodeJS with Express and Mongoose in CoffeeScript. We implemented four CRUD operations for a single resource along with a simple error handling mechanism.
The code of this application is available on Github. No additional changes are needed to deploy it on Heroku, either using MongoHQ or MongoLab.