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'
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
src/index.js
const express = require('express')
const app = express()
module.exports = app
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)
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"
},
...
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
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
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
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
This would work just fine! If I visit http://localhost:3000/v1
this thing will have voice now:
{"message":"Let's TODO!"}
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
*/
...
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> at app.use (/develop/another-todo-api/src/index.js:9:11)<br> at Layer.handle [as handle_request] (/develop/another-todo-api/node_modules/express/lib/router/layer.js:95:5)<br> at trim_prefix (/develop/another-todo-api/node_modules/express/lib/router/index.js:312:13)<br> at /develop/another-todo-api/node_modules/express/lib/router/index.js:280:7<br> at Function.process_params (/develop/another-todo-api/node_modules/express/lib/router/index.js:330:12)<br> at next (/develop/another-todo-api/node_modules/express/lib/router/index.js:271:10)<br> at expressInit (/develop/another-todo-api/node_modules/express/lib/middleware/init.js:33:5)<br> at Layer.handle [as handle_request] (/develop/another-todo-api/node_modules/express/lib/router/layer.js:95:5)<br> at trim_prefix (/develop/another-todo-api/node_modules/express/lib/router/index.js:312:13)<br> at /develop/another-todo-api/node_modules/express/lib/router/index.js:280:7
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
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"}
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
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'))
...
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
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
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"
}
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"
},
...
Time to try it.
npm run lint
> another-todo-api@0.0.0 lint /develop/another-todo-api
> eslint **/*.js
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
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
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.
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.
Top comments (8)
Me gusta el tratamiento que haces de los eventos de sever.listen() normalmente en los tutoriales se omite y luego crash en producciΓ³n.
Muchas gracias por tu comentario y por la lectura
Vas a aΓ±adir algΓΊn Logger? Has probado prettier + eslint?
SΓ, me gustarΓa implementar
winston
en un futuro.No conocΓa
prettier
, le echare un ojo, muchas gracias.This is awesome, thanks! I was looking for something exactly like this earlier today, but couldn't find anything recent.
Glad to read it! Hope it helped you.
Excelente aporte Alberto!
Muchas gracias por comentar y por la lectura.