Creating a REST API in node.js

Getting started with making web APIs can be confusing, even overwhelming at first. I'd like to share my process for creating APIs in Node.js.

The server

First let's create a package.json and add a dependency:

terminal

cd rest_api
npm init -y
npm i express

This is a simple 'hello world' with express:

index.js

const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world!')
})
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Express started on http://localhost:${port}\npress Ctrl+C to terminate.`)
})

Express is imported, then an instance is created and saved as app.

index.js

const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world!')
})
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Express started on http://localhost:${port}\npress Ctrl+C to terminate.`)
})

app.get() tells the Express instance to listen for GET requests to the specified route; here it's just /. When this route is requested, the given callback is fired, in this case it sends the string "Hello world!" as a response.

index.js

const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world!')
})
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Express started on http://localhost:${port}\npress Ctrl+C to terminate.`)
})

port is declared as a variable which can be set from the environment, or otherwise defaults to 3000 if none is specified. Express then listens on that port, and after the server has finally started it uses console.log to print a message in the terminal.

index.js

const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world!')
})
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Express started on http://localhost:${port}\npress Ctrl+C to terminate.`)
})

This is a simple 'hello world' with express:

Express is imported, then an instance is created and saved as app.

app.get() tells the Express instance to listen for GET requests to the specified route; here it's just /. When this route is requested, the given callback is fired, in this case it sends the string "Hello world!" as a response.

port is declared as a variable which can be set from the environment, or otherwise defaults to 3000 if none is specified. Express then listens on that port, and after the server has finally started it uses console.log to print a message in the terminal.

index.js

const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('Hello world!')
})
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Express started on http://localhost:${port}\npress Ctrl+C to terminate.`)
})

Assuming everything went okay, you should then be able to run node index.js and point your browser at http://localhost:3000/ and see the message "Hello world!"


Another way to test this instead of opening your browser is with either cURL or postman. cURL is great if you're the command-line junkie type (like me), postman is really pretty and slick but a bit too much hassle for me.

Once you have curl installed it should be as simple to get your Hello World in the shell with:

terminal

curl localhost:3000

Request parameters

A server that only responds with the same thing every time isn't very fun though. Let's make a new route that allows us to say hello when passed a name:

index.js

const app = express()
app.get('/', (req, res) => {
res.send('Hello world!')
})
app.get('/:name', (req, res) => {
res.send(`Hello ${req.params.name}!`)
})

You'll want to add this before the app.listen() command so that it gets registered properly.

Then, with curl:

terminal

curl localhost:3000/foo

You should see "Hello foo!"

Automatic restart

To get the above change working, you first have to stop the server and then restart it again, which can get fairly annoying after a few changes. nodemon can be installed to watch for changes and restart automatically.

terminal

npm i nodemon

Then alter your package.json so that it looks like this:

package.json

"scripts": {
"start": "nodemon -w '*' -x 'node index.js'",
"test": "echo \"Error: no test specified\" && exit 1"
},

Now you can run npm start and it will use nodemon to watch for changes and restart the server.

The database

Before making a database, you should first identify your data and what you'll be doing with it. I spent about 30 seconds debating what kind of data to use in this tutorial, and decided to do "something" with movies.

Among other things, a movie would have a title and a date it was released, and that seems like enough to work with for now.

I don't want to go into setting up and configuring a big fancy database, so we will SQLite, a RDBMS that uses files to represent databases, and doesn't require a background processes to store or retrieve data. If you prefer PostgreSQL or MariaDB feel free to use those, you should only have to adjust a couple of things.

terminal

npm i sqlite3 knex

Rather than interacting with the database directly, we're going to use a query builder called knex. knex allows you to write your queries in plain JavaScript, which provides an abstraction layer over your database driver. This makes it so your queries aren't necessarily tied to a specific database engine, and if you want to change to a different database later it's a much less difficult migration, sometimes only just a few lines.

knex has a really great cli. We can use it to create a default config:

terminal

npx knex init

Have a look at the file it creates at knexfile.js

In a new file, perhaps called db.js, save this:

db.js

const knex = require('knex')
const config = require('./knexfile.js')
const env = process.env.NODE_ENV || 'development'
module.exports = knex(config[env])

This creates and exports a single knex instance that we can re-use in other modules.

Now let's define a new table using the migrations cli:

terminal

npx knex migrate:make create_movies

This will create migrations/<timestamp>_create_movies.js with a few empty functions, change it so that it looks like this:

migrations/01-create_movies.js

exports.up = async knex => {
return knex.schema.createTable('movies', t => {
t.increments('id').primary()
t.string('title')
t.integer('released')
})
}
exports.down = async knex => {
return knex.schema.dropTableIfExists('movies')
}

Now, we can use the cli again to create our table:

terminal

npx knex migrate:latest

Data methods

Now that we have a database and a table, we need some methods to populate the table with data.

movies.js

const db = require('./db')
async function create({ title, released }) {
return db('movies').insert({ title, released })
}

This creates a function called create that accepts an object with keys title and released, which are purposely the same name as the field names.

Now that we can create movies, we should make a way to list them.

movies.js

const db = require('./db')
async function create({ title, released }) {
return db('movies').insert({ title, released })
}
async function list() {
return db('movies').select('*')
}

To finish up, since this file won't directly consume these functions, we need to export them so they can be used as a module.

movies.js

const db = require('./db')
async function create({ title, released }) {
return db('movies').insert({ title, released })
}
async function list() {
return db('movies').select('*')
}
module.exports = {
create,
list,
}

This creates a function called create that accepts an object with keys title and released, which are purposely the same name as the field names.

Now that we can create movies, we should make a way to list them.

To finish up, since this file won't directly consume these functions, we need to export them so they can be used as a module.

movies.js

const db = require('./db')
async function create({ title, released }) {
return db('movies').insert({ title, released })
}

Wiring it up

Now let's wire up our database to our server!

Going back to our index.js, let's import our new movies file and make a route to use the list method so that it looks like this:

index.js

const express = require('express')
const movies = require('./movies')
const app = express()
app.get('/api/v1/movies', async (req, res) => {
res.json(await movies.list())
})
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Express started on http://localhost:${port}\npress Ctrl+C to terminate.`)
})

Requesting the data is the easy part, with curl we can simply:

terminal

curl localhost:3000/api/v1/movies

which right now will return an empty JSON array.

Express middleware

To be able to properly create movies, we need to introduce a piece of express middleware.

Middleware are functions that are run before a route handler. They usually alter the request or response objects in some way. There are tons of middleware modules for express, if you'd like to read more you can check out the offical docs.

index.js

app.use(express.urlencoded({ extended: false }))

Now we can add a route that responds to POST requests, and our body data will be available as req.body:

index.js

const express = require('express')
const movies = require('./movies')
const app = express()
app.use(express.urlencoded({ extended: false }))
app.get('/api/v1/movies', async (req, res) => {
res.json(await movies.list())
})
app.post('/api/v1/movies', async (req, res) => {
const { title, released } = req.body
res.json(await movies.create({ title, released }))
})
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Express started on http://localhost:${port}\npress Ctrl+C to terminate.`)
})

Using curl we can now insert data with:

terminal

curl -X POST -d 'released=2017' -d 'title=nothing good' localhost:3000/api/v1/movies

Which will return the new number of rows.

Full CRUD support

Any good API endpoint usually provides four methods:

  • Create
  • Read
  • Update
  • Delete

We only have the first two, so let's finish it up and add the others.

Back in our movies.js let's add two more functions:

movies.js

const db = require('./db')
async function create({ title, released }) {
return db('movies').insert({ title, released })
}
async function list() {
return db('movies').select('*')
}
async function update({ id, title, released }) {
return db('movies').update({ title, released }).where({ id })
}
async function del(id) {
return db('movies').where({ id }).del()
}

Then we need to update our exports:

movies.js

module.exports = {
create,
list,
update,
del,
}

Back in the index.js file we need to make two more routes:

index.js

const express = require('express')
const movies = require('./movies')
const app = express()
app.use(express.urlencoded({ extended: false }))
app.get('/api/v1/movies', async (req, res) => {
res.json(await movies.list())
})
app.post('/api/v1/movies', async (req, res) => {
const { title, released } = req.body
res.json(await movies.create({ title, released }))
})
app.put('/api/v1/movies', async (req, res) => {
await movies.update(req.body)
res.json(await movies.list())
})
app.delete('/api/v1/movies', async (req, res) => {
await movies.del(req.body.id)
res.json(await movies.list())
})
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Express started on http://localhost:${port}\npress Ctrl+C to terminate.`)
})

Now we can edit movies by passing field names as data pieces in curl:

terminal

curl -X PUT -d 'id=1' -d 'title=new title' localhost:3000/api/v1/movies

And delete them by passing the id:

terminal

curl -X DELETE -d 'id=1' localhost:3000/api/v1/movies

Conclusion

We learned how to make a single endpoint respond to different actions and query a database with the appropriate methods.

Comments