API Routing with Express
Note: This is the 2nd post of a series of post about Building APIs With Express.
Based on my last post about Making APIs with Node and Express I'll continue developing over the generated code.
So, I left the basement for my TODO API prepared. Now it's time to work in the different endpoints and HTTP verbs/methods that this API is going to use.
/v1/tasks
I'm going to start building my API's endpoints with Tasks Collection.
[GET] /v1/tasks
First thing I need is to GET
the list of tasks from my so innovative TODO API and I think that the best way to build it is by creating a new isolated express router instance.
src/v1/tasks.js
const router = require("express").Router();
/**
* TODO: Store data in DB.
*/
let tasks = [
{
description: "Another task",
isDone: false,
createdAt: Date.now(),
},
];
router
.route("/")
.get((req, res, next) => {
return res.json(tasks);
});
module.exports = router;
Easy peasy! Now I need to mount that router on my API, I'm going to remove the old Let's TODO!
message:
src/v1/index.js
const router = require("express").Router();
const tasks = require("./tasks");
router.use("/tasks", tasks);
module.exports = router;
Let's try the new endpoint (Now that I have implemented yarn
I can run the server with `` yarn start
instead of npm start
):
curl -X GET localhost:3000/v1/tasks
[
{
"createdAt" : 1481985039988,
"isDone" : false,
"description" : "Another task"
}
]
Note: It's needed to restart the server if there have been changes in the code while it is running to see the changes.
Now I'll continue with the creation of a new task
[POST] /v1/tasks
It should be in the same router instance the GET
method is. This time I'll need a new middleware to parse correctly the request body, body-parser (this time I'll install it with Yarn).
yarn add body-parser
Note: Same as execute npm i -S body-parser
And now it needs to be attached to the app
src/index.js
const express = require('express')
const logger = require('morgan')
const bodyParser = require('body-parser')
const app = express()
const v1 = require('./v1')
/**
* Middlewares
*/
app.use(logger('dev'))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
...
Note: It will inject data from the request into req.body
.
And now the request handler.
src/v1/tasks.js
...
router.route('/')
.get((req, res, next) => {
return res.json(tasks)
})
.post((req, res, next) => {
const newTask = req.body
newTask.createdAt = Date.now()
newTask.isDone = false
tasks.push(newTask)
return res.status(201).json(newTask)
})
module.exports = router
Note: Take advantage of the HTTP status codes.
Done, let's try again with a POST
request this time:
curl -X POST -H "Content-Type: application/json" --data '{"description": "Also another task more"}' localhost:3000/v1/tasks
{"description":"Also another task more","createdAt":1481986821539}
curl -X GET localhost:3000/v1/tasks
[
{
"createdAt" : 1481986807819,
"isDone" : false,
"description" : "Another task"
},
{
"createdAt" : 1481986821539,
"isDone" : false,
"description" : "Also another task more"
}
]
Time to delete! Due this is an endpoint pointing a collection when it receives a DELETE
request it should delete all items of the collection.
[DELETE] /v1/tasks
src/v1/tasks.js
...
.post((req, res, next) => {
const newTask = req.body
newTask.createdAt = Date.now()
tasks.push(newTask)
return res.json(newTask)
})
.delete((req, res, next) => {
tasks = []
res.status(204).end()
})
module.exports = router
Delete all the things!
curl -X DELETE -i localhost:3000/v1/tasks
HTTP/1.1 204 No Content
X-Powered-By: Express
ETag: W/"2-11FxOYiYfpMxmANj4kGJzg"
Date: Sat, 17 Dec 2016 17:13:07 GMT
Connection: keep-alive
I am done with this endpoint.
/v1/tasks/:taskId
Now it's time to handle the single task endpoint. Here I'm going to take advantage of an Express feature to parse URLs, for this case, to treat a segment of the URL as a parameter and assigning it the name taskId
.
taskId
param
I am going to define a new param for the tasks router to get a certain task passing the task ID in the URL.
src/v1/tasks.js
...
router.route('/')
...
router.param('taskId', (req, res, next, id) => {
const task = tasks[id]
let err
if (!task) {
err = new Error('Task not found')
err.status = 404
} else {
req.task = task
}
return next(err)
})
module.exports = router
[GET] /v1/tasks/:taskId
Then I only need to reply with the found task in a new endpoint listening to any request aimed to /v1/tasks/:taskId
meaning by :taskId
whatever comes after the slash (IE: /v1/tasks/my-task-id
OR /v1/tasks/01234
).
src/v1/tasks.js
...
router.param('taskId', (req, res, next, id) => {
...
})
router.route('/:taskId')
.get((req, res, next) => {
return res.json(req.task)
})
module.exports = router
Let's try it
curl -X GET localhost:3000/v1/tasks/0
{
"description" : "Another task",
"isDone" : false,
"createdAt" : 1481996187751
}
curl -X GET -i localhost:3000/v1/tasks/1234
HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 1082
ETag: W/"43a-4d6NK29IKrV0B3jSAdQGvA"
Date: Sat, 17 Dec 2016 17:46:36 GMT
Connection: keep-alive
{
"stack" : "Error: Task not found\n at router.param (/develop/another-todo-api/src/v1/tasks.js:38:11)\n at paramCallback (/develop/another-todo-api/node_modules/express/lib/router/index.js:404:7)\n at param (/develop/another-todo-api/node_modules/express/lib/router/index.js:384:5)\n at Function.process_params (/develop/another-todo-api/node_modules/express/lib/router/index.js:410:3)\n at next (/develop/another-todo-api/node_modules/express/lib/router/index.js:271:10)\n at Function.handle (/develop/another-todo-api/node_modules/express/lib/router/index.js:176:3)\n at router (/develop/another-todo-api/node_modules/express/lib/router/index.js:46:12)\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",
"message" : "Task not found"
}
[POST] /v1/tasks/:taskId
Now it's important to pay attention here because the POST
request to a specified resource in an API REST by definition should override completely the resource, meaning that if:
I do a GET
to /v1/tasks/0
receiving:
{
"description": "Another task",
"isDone": false,
"createdAt": 1481996187751
}
If I do a POST
to /v1/tasks/0
with this data:
{ "isDone": true }
The next time I do a GET
to /v1/tasks/0
I'll receive the next response:
{ "isDone": true }
The proper way a client should make a POST
to an API resource is by providing all the resource info in the request to avoid the lose of info.
Now go back to code!
src/v1/tasks.js
...
.post((req, res, next) => {
const updatedTask = req.body
tasks[req.params.taskId] = updatedTask
return res.json(updatedTask)
})
...
[PATCH] /v1/tasks/:taskId
Now the PATCH
request is the one used to update partially a resource in an API REST.
src/v1/tasks.js
...
.patch((req, res, next) => {
for (let prop in req.body) {
tasks[req.params.taskId][prop] = req.body[prop]
}
return res.json(tasks[req.params.taskId])
})
...
Let's try this one with curl
:
curl -X PATCH -H "Content-Type: application/json" --data '{"isDone": true}' localhost:3000/v1/tasks/0
{
"isDone" : true,
"description" : "Another task",
"createdAt" : 1481998868351
}
Oh yeah!
[DELETE] /v1/tasks/:taskId
I think there's no much to explain here, is pretty much the same as the DELETE
for the whole collections instead that this one only deletes one resource.
src/v1/tasks.js
...
.delete((req, res, next) => {
tasks.splice(req.params.taskId, 1)
res.status(204).end()
})
...
End of the route
Well, I think that with this the awesome Another TODO API is completely functional, maybe I should care about that the info isn't being stored anywhere and every time the server stops I lose it all, but that'll be another time!
Comment and check the code on GitHub!