DEV Community

Alberto Fernandez Medina
Alberto Fernandez Medina

Posted on • Originally published at onlythepixel.com

Making APIs with Node and Express

I'm going to make a simple API with Node and Express, specifically an API for a TODOs app.

This post was first published on my blog, onlythepixel.com

Project Boilerplate

So I start a new Node project with the name another-todo-api in my terminal

mkdir another-todo-api && cd $_
git init
echo 'Another boring TODO API' > README.md
npm init -y
echo 'node_modules
*.log' >> .gitignore
npm i -S express
git add .
git commit -m 'First commit'
Enter fullscreen mode Exit fullscreen mode

Note: npm i -S is the same as npm install --save but in the shorter way.

Simple! I've started a new git repo with an empty README file and a new npm package that has express as a dependency. Let's play a little with Express.

I like to have all my source code inside a src folder:

mkdir src
touch src/index.js
Enter fullscreen mode Exit fullscreen mode

src/index.js

const express = require('express')
const app = express()

module.exports = app
Enter fullscreen mode Exit fullscreen mode

Note: Due to the coolness of this article all the javascript code will be shown in ES2015 (So it's recommended to use Node v6 or later) and I'll be using Standard Code Style for the javascript.

Now to run the server I don't like to start it from the index.js file directly, instead, I prefer to run it through an external bin file (like Express does in its generator).

bin/www

#!/usr/bin/env node
/**
 * Created from https://github.com/expressjs/generator/blob/d07ce53595086dd07efb100279a7b7addc059418/templates/js/www
 */

/**
 * Module dependencies.
 */
const http = require('http')
const debug = require('debug')('another-todo:server')
const app = require('../src')

/**
 * Get port from environment and store in Express.
 */

const port = normalizePort(process.env.PORT || '3000')
app.set('port', port)

/**
 * Create HTTP server.
 */
const server = http.createServer(app)

/**
 * Normalize a port into a number, string, or false.
 */
function normalizePort (val) {
  const port = parseInt(val, 10)

  // named pipe
  if (isNaN(port)) return val

  // port number
  if (port >= 0) return port

  return false
}

/**
 * Event listener for HTTP server "error" event.
 */
function onError (error) {
  if (error.syscall !== 'listen') throw error

  const bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges')
      process.exit(1)
      break
    case 'EADDRINUSE':
      console.error(bind + ' is already in use')
      process.exit(1)
      break
    default:
      throw error
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */
function onListening () {
  const addr = server.address()
  const bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port
  debug('Listening on ' + bind)
}

/**
 * Listen on provided port, on all network interfaces.
 */
server.listen(port)
server.on('error', onError)
server.on('listening', onListening)
Enter fullscreen mode Exit fullscreen mode

And then bind this file to my npm scripts.

package.json

  ...
  "scripts": {
    "start": "set DEBUG=another-todo:* && node bin/www",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  ...
Enter fullscreen mode Exit fullscreen mode

Also I'll need the package debug as dependency of my project due that I'm using it in my www file:

npm i -S debug
Enter fullscreen mode Exit fullscreen mode

After that I can try my brand new Express server:

npm start

> another-todo-api@0.0.0 start /develop/another-todo-api
> set DEBUG=another-todo:* && node bin/www

  another-todo:server Listening on port 3000 +0ms
Enter fullscreen mode Exit fullscreen mode

By default this little guy should be listening on the port 3000 of my computer. If I access with some browser to http://localhost:3000 I'll receive a sad Cannot GET /.

Express Router

Time to make this guy have some voice to being able to reply me when I ask for something. For that I'll use the Express Routers to build up my TODO API pieces.

src/v1/index.js

const router = require('express').Router()

router.route('/')
  .get((req, res, next) => {
    return res.json({
      message: 'Let\'s TODO!'
    })
  })

module.exports = router
Enter fullscreen mode Exit fullscreen mode

Note: that thing of v1 is because it's a good practice to implement a version system in APIs.

Just a simple reply to a GET request, if I go to http://localhost:3000 again, nothing happens... Because I need to mount this router path in my Express app.

src/index.js

const express = require('express')
const app = express()
const v1 = require('./v1')

/**
 * Routes
 */
app.use('/v1', v1)

module.exports = app
Enter fullscreen mode Exit fullscreen mode

This would work just fine! If I visit http://localhost:3000/v1 this thing will have voice now:

{"message":"Let's TODO!"}
Enter fullscreen mode Exit fullscreen mode

Middlewares

Now I'm going to add some middleware to avoid contact with Systems that doesn't support JSON format.

src/index.js

const express = require('express')  
const app = express()  
const v1 = require('./v1')

/**
 * Ensure JSON acceptance
 */
app.use((req, res, next) => {
  let err

  if (!req.accepts('json')) {
    err = new Error('Not Acceptable')
    err.status = 406
  }

  return next(err)
})

/**
 * Routes
 */
...
Enter fullscreen mode Exit fullscreen mode

Now that I have a middleware that is returning an error I can test it with curl (probably you already have it in your terminal).

curl -i -H "Accept: text" localhost:3000

HTTP/1.1 406 Not Acceptable
X-Powered-By: Express
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Content-Length: 1052
Date: Sun, 11 Dec 2016 18:40:03 GMT
Connection: keep-alive

Error: Not Acceptable<br> &nbsp; &nbsp;at app.use (/develop/another-todo-api/src/index.js:9:11)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/develop/another-todo-api/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at trim_prefix (/develop/another-todo-api/node_modules/express/lib/router/index.js:312:13)<br> &nbsp; &nbsp;at /develop/another-todo-api/node_modules/express/lib/router/index.js:280:7<br> &nbsp; &nbsp;at Function.process_params (/develop/another-todo-api/node_modules/express/lib/router/index.js:330:12)<br> &nbsp; &nbsp;at next (/develop/another-todo-api/node_modules/express/lib/router/index.js:271:10)<br> &nbsp; &nbsp;at expressInit (/develop/another-todo-api/node_modules/express/lib/middleware/init.js:33:5)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/develop/another-todo-api/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at trim_prefix (/develop/another-todo-api/node_modules/express/lib/router/index.js:312:13)<br> &nbsp; &nbsp;at /develop/another-todo-api/node_modules/express/lib/router/index.js:280:7
Enter fullscreen mode Exit fullscreen mode

Note: If I try it without the --header "Accept: text" it will reply my with the correct response.

Mind your language young man! It's answering me in HTML I need to parse that reply passing it through a Error Handler .

ErrorHandler

Now that my app has errors (in the good meaning) I need a ErrorHandler on my app.

src/index.js

...
/**
 * Routes
 */
app.use('/v1', v1)

/**
 * ErrorHandler
 */
app.use((err, req, res, next) => {
  res.status(err.status || 500)
    .json({
      message: err.message,
      stack: err.stack
    })
})

module.exports = app
Enter fullscreen mode Exit fullscreen mode

Note: It's important to remember only to use that ErrorHandler only in development and try not to show so many info when it's a production environment.

If I ask my server again.

curl -i -H "Accept: text" localhost:3000

HTTP/1.1 406 Not Acceptable
X-Powered-By: Express
X-Content-Type-Options: nosniff
Content-Type: text/html; charset=utf-8
Content-Length: 1052
Date: Sun, 11 Dec 2016 18:42:12 GMT
Connection: keep-alive

{"message":"Not Acceptable","stack":"Error: Not Acceptable\n    at app.use (/develop/another-todo-api/src/index.js:9:11)\n    at Layer.handle [as handle_request] (/develop/another-todo-api/node_modules/express/lib/router/layer.js:95:5)\n
    at trim_prefix (/develop/another-todo-api/node_modules/express/lib/router/index.js:312:13)\n    at /develop/another-todo-api/node_modules/express/lib/router/index.js:280:7\n    at Function.process_params (/develop/another-todo-api/node_modules/express/lib/router/index.js:330:12)\n    at next (/develop/another-todo-api/node_modules/express/lib/router/index.js:271:10)\n    at expressInit (/develop/another-todo-api/node_modules/express/lib/middleware/init.js:33:5)\n    at Layer.handle [as handle_request] (/develop/another-todo-api/node_modules/express/lib/router/layer.js:95:5)\n    at trim_prefix (/develop/another-todo-api/node_modules/express/lib/router/index.js:312:13)\n    at /develop/another-todo-api/node_modules/express/lib/router/index.js:280:7"}
Enter fullscreen mode Exit fullscreen mode

Now that's a good error reply.

Extras

I leaved some things pending on my code when building my API server, you can skip this part if you feel lazy about continue reading this crap.

Logging with Morgan

There are tons of middlewares packages for express, one very useful is Morgan, it's an HTTP request logger and it'll print in the terminal all the request that the server will receive.

npm i -S morgan
Enter fullscreen mode Exit fullscreen mode

Then I need to attach it to my app.

src/index.js

const express = require('express')
const logger = require('morgan')
const app = express()
const v1 = require('./v1')

/**
 * Middlewares
 */
app.use(logger('dev'))

...
Enter fullscreen mode Exit fullscreen mode

Now if I run my server and make some requests to it:

npm start

> another-todo-api@0.0.0 start /develop/another-todo-api
> set DEBUG=another-todo:* && node bin/www

  another-todo:server Listening on port 3000 +0ms
GET / 404 5.469 ms - 13
GET /favicon.ico 404 0.905 ms - 24
GET /v1 200 2.275 ms - 25
Enter fullscreen mode Exit fullscreen mode

Linting

I said that I was using Standar code style fot the javascript code but I didn't bother to make me sure that this code style get used every time someone writes code on this project. The best way to do this is with some linter and for this I'm going to use ESLint.

First I need to install my development dependencies (because this tools are not going to be used in production):

npm i -D eslint eslint-config-standard eslint-plugin-standard eslint-plugin-promise
Enter fullscreen mode Exit fullscreen mode

Note: npm i -D is the same as npm install --save-dev.

Now I need to define some configuration file on my project code.

.eslintrc

{
  "extends": "standard"
}
Enter fullscreen mode Exit fullscreen mode

Note: Just that!

And I'm going to add a new npm script.

package.json

...
  "scripts": {
    "lint": "eslint **/*.js",
    "start": "set DEBUG=another-todo:* && node bin/www",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
...
Enter fullscreen mode Exit fullscreen mode

Time to try it.

npm run lint

> another-todo-api@0.0.0 lint /develop/another-todo-api
> eslint **/*.js
Enter fullscreen mode Exit fullscreen mode

Note: If nothing happens is because there's no errors, you can try to reproduce an error by adding some ; in some of the JS files.

There are several plugins for linting the code on the fly in the text editor, so by this way you don't need to run the linting npm script. In my case I use Atom with linter and linter-eslint packages.

Editorconfig

This is a very important tool, it avoids a lot of noise between commits on git or git diffs. Also it helps to keep the code format homogeneous among the project.

.editorconfig

# EditorConfig is awesome: http://EditorConfig.org

root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false
Enter fullscreen mode Exit fullscreen mode

As for the linting there is also plugins available for the usuals text editors. In the case of Atom there is the editorconfig package.

Yarn

Not long ago Yarn, a new dependency manager, got released and it's fully compatible with npm. Only needs to be installed and then just:

yarn
Enter fullscreen mode Exit fullscreen mode

Note: It is the same as yarn install that is the same as npm install. You can check the Yarn vs. NPM command comparison.

There will appear a new file called yarn.lock that's info used by Yarn to improve the timing installing dependencies and if you red the first lines of the file all will be Crystal Clear:

# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
Enter fullscreen mode Exit fullscreen mode

From here I can start using Yarn instead NPM for dependencies and NPM scripts.

Enough!!!

This post is long enough to bore you so I'm going to stop here. Later!

Oh! Yes... you can check this code on the another-todo-api GitHub repo.

Latest comments (8)

Collapse
 
mrm8488 profile image
Manuel Romero

Me gusta el tratamiento que haces de los eventos de sever.listen() normalmente en los tutoriales se omite y luego crash en producción.

Collapse
 
albertofdzm profile image
Alberto Fernandez Medina

Muchas gracias por tu comentario y por la lectura

Collapse
 
mrm8488 profile image
Manuel Romero

Vas a añadir algún Logger? Has probado prettier + eslint?

Thread Thread
 
albertofdzm profile image
Alberto Fernandez Medina

Sí, me gustaría implementar winston en un futuro.

No conocía prettier, le echare un ojo, muchas gracias.

Collapse
 
janguianof profile image
Jaime Anguiano

Excelente aporte Alberto!

Collapse
 
albertofdzm profile image
Alberto Fernandez Medina

Muchas gracias por comentar y por la lectura.

Collapse
 
adamfriedl profile image
Adam Friedl

This is awesome, thanks! I was looking for something exactly like this earlier today, but couldn't find anything recent.

Collapse
 
albertofdzm profile image
Alberto Fernandez Medina

Glad to read it! Hope it helped you.