APIs are commonly a pain to build - there's all kinds of moving pieces: routing, data parsing, serialization, database interactions, etc. For years I've struggled with writing web APIs in Node.js - there's just so much complexity required in an API boilerplate. That's why I wrote Simple API. Simple API is an incredibly simple module for writing web-based APIs in Node.js
I often hear people say that they've written a library that makes something "simple", but in reality they've just made it a different kind of complex. I'm going to prove that's not the case with Simple API. This tutorial is going to teach you to write an API for a ToDo list, where the entire project is less than 300 lines of code. That's the entire project - the API related files are less than 200 lines of code.
The examples will all be written in CoffeeScript, but can easily be compiled to Javascript
Of course, every module requires a little bit of time to install and setup the boilerplate. That's where we'll start. First off, you need to install Simple API. This is very easy using NPM.
npm install simple-api
See, that was simple! Now we need to include the module and run some simple setup code. Put the code below into your index.coffee
file.
#Load the Simple API Module
api = require 'simple-api'
#Create API Server
v0 = new api
prefix: ["api", "v0"]
host: "localhost"
port: "3333"
logLevel: 0
Pretty simple, but let's talk through each of those lines.
api = require 'simple-api'
: This loads the Simple API module. The api
object here is a class waiting to be instantiated.
v0 = new api
: This instantiates the API object. I'm calling this API 'v0', so I named the variable that. Everything after this line is in the config object for the API
prefix: ["api", "v0"]
: This is the path that will prefix all API calls. Each element in the array is a part of the URL. This would match "http://host/api/v0". You can also use a string to configure the prefix, like "api/v0"
.
host: "localhost"
: This is the host that the API should be listening on. If you want to listen to all hosts, set this to null
port: 3333
: This is the port that the API will be listening on. If you want to listen on port 80, set this to null
.
logLevel: 0
: Simple API has 5 log levels. Lower numbers mean less logs.
There you go! If you run coffee index
you will see the server startup, and Simple API will output a log that it's listening for requests.
Unfortunately, as you'll notice, your API doesn't actually do anything at this point. If you make any requests to it you will get a 404 Not Found
response. That's because you don't have any Controllers. Controllers are a pretty broad way to represent any object that your API can talk about. You might have a Controller for a user, a task, a tweet - any "Object". For the ToDo app, we will have a single controller for tasks.
I prefer to organize my API by creating an api
folder, with a version
folder, and then place controllers
and models
folders inside of that. So my controllers can be found in ./api/v0/controllers
.
Controllers have three different things that need to be configured. Routes tell Simple API which URLs are related to the controller. Routes also tell Simple API which part of each URL should be parsed as a parameter. Actions are the meat of the controller - they decide what happens for each API request. Helpers are functions that may need to be called from multiple actions. There's also an options key, but for now it is not used for anything (but are accessable from actions, if you want them).
I like to start my controllers by writing the routes. This allows me to think in an abstract way about how I want people to interact with my API, rather than getting distracted by the technical implementation for each action. First, though, we need to write the overall structure of a controller definition:
TasksController =
options: {} #This doesn't get used
routes: {}
actions: {}
helpers: {}
Now we can get started writing the routes. It's important to name your routes descriptively, because they will need to match the name of your actions. When you come back to maintain your code, the name of the routes and actions are the easiest way to find the section you're looking for.
TasksController =
options: {}
routes:
getAllTasks:
method: "GET"
path: []
This is the most simple type of route you can get. It uses the GET
method, and has no path. That means it will respond from /api/v0/tasks
. It is named getAllTasks
, so when an API call matches this route it will call the getAllTasks action. You'll notice that I'm using an array for the path definition here, similar to the API prefix definition; you can use a string instead.
getTask:
method: "GET"
path: ["*identifier"]
The getTask
route is similar to getAllTasks
, but adds a little spice. This route will allow the user to request a single task by ID. You can see that I've defined a single piece to the path: *identifier
. The * tells Simple API to match any alphanumeric string for that portion of the URL. There are other match types for alpha-only, numeric-only, or RegExp matching. You can read about those in the docs. Whatever matches for this first portion of the URL will be stored in a variable called identifier
. You could name that variable anything, but identifier
makes the most sense here.
getCategoryTasks:
method: "GET"
path: ["category", "*category"]
getCategoryTasks
again builds upon the previous route. This route has a static string match of category
in the front, and then another alphanumeric parameter match on the end. That means it will match URLs like http://host/api/v0/category/4lph4Num3r1C
. The matched string in the second part of the URL will be stored in the category
variable.
createTask:
method: "POST"
path: []
The createTask
route looks pretty basic, but it has one important change. The HTTP method is now POST
. You can define any HTTP method you want with Simple API, but there are four commonly used methods: POST is for Creating data, GET is for Reading data, PUT is for Updating data, and DELETE is for Deleting data. These can easily be remembered as CRUD. The rest of the routes will build upon the ideas that you already know.
updateTask:
method: "PUT"
path: ["*identifier"]
deleteTask:
method: "DELETE"
path: ["*identifier"]
completeTask:
method: "PUT"
path: ["*identifier", "complete"]
Great! Now we've got all of our routes defined, and can move on to the actual action code. As I've mentioned, the names of your actions need to be the same as your route names. This is how Simple API knows which action to call for which route. Each action receives three parameters: req
, res
, and params
(naturally, you can name them anything). req
and res
are the HTTP request and response objects, respectively. The params
object is a key-value Object of all the matches from your route. For instance, getCategoryTasks
will have params.category
set from the second key in the URL. These keys will always be defined, otherwise the route would not have gotten called.
TasksController =
options: {}
routes: #Removed to save space
actions:
getAllTasks: (req, res, params) ->
Tasks = mongoose.model "Tasks"
Tasks.getAll (err, allTasks) =>
if err
console.log err
@responses.internalError res
else
@responses.respond res, allTasks
Most of the code in that action is interacting with the Model, which is not important for this tutorial. Simple API is still early in its development, so it does not officially support any structured form of models. It will in the future, but for now you have to write your own models. The important parts of this action are those two @responses
lines. These are convenience functions that have been defined so you don't have to write your own response handling. There are five convenience functions: internalError
, notAuth
, notAvailable
, redirect
, and respond
. Each of them requires res
as the first parameter, and accepts a response body as the second parameter. This response can either be a string or an Object that will be turned into JSON.
getTask: (req, res, params) ->
Tasks = mongoose.model "Tasks"
Tasks.getById params.identifier, (err, task) =>
if err
console.log err
@responses.internalError res
else
if task
@responses.respond res, task
else
@responses.notAvailable res
This action is very similar to the getAll
action, but it gets the task ID from params.identifier
. As I mentioned, you don't need to check if the parameter exists. The action could not have been called if the parameters didn't exist. The rest of the actions will follow a similar format to these past two actions.
getCategoryTasks: (req, res, params) ->
Tasks = mongoose.model "Tasks"
Tasks.getAllFromCategory params.category, (err, catTasks) =>
if err
console.log err
@responses.internalError res
else
@responses.respond res, catTasks
createTask: (req, res, params) ->
Tasks = mongoose.model "Tasks"
data = ""
req.on 'data', (chunk) ->
data += chunk
req.on 'end', () =>
#You should do this in a try/catch, but I'm leaving it simple for the example
taskInfo = JSON.parse data
Tasks.create taskInfo, (err, task) =>
if err
console.log err
@responses.internalError res
else
@responses.respond res, task
updateTask: (req, res, params) ->
Tasks = mongoose.model "Tasks"
data = ""
req.on 'data', (chunk) ->
data += chunk
req.on 'end', () =>
#You should do this in a try/catch, but I'm leaving it simple for the example
taskInfo = JSON.parse data
Tasks.updateById params.identifier, taskInfo, (err, task) =>
if err
console.log err
@responses.internalError res
else
@responses.respond res, task
deleteTask: (req, res, params) ->
Tasks = mongoose.model "Tasks"
Tasks.deleteById params.identifier, (err) =>
if err
console.log err
@responses.internalError res
else
@responses.respond res
completeTask: (req, res, params) ->
Tasks = mongoose.model "Tasks"
Tasks.getById params.identifier, (err, task) =>
if err
console.log err
@responses.internalError res
else
if task.completed
#If the task is already completed, just return a 200 because it's already done
@responses.respond res
else
task.completed = true
task.save (err) =>
if err
@responses.internalError res
else
@responses.respond res
The only interesting difference here is in the completeTask
action. You can see there that I'm doing a bit more logic than in the other controllers. I'm first fetching the task from my model, then checking to see if it is already completed. If it is, I respond immediately - otherwise I update the task and send the response.
If you were to run this project right now, you might notice one very major bug. I am using MongoDB for the database, and using Mongo's built-in ObjectId type as the identifier. ObjectIds are 24 character alphanumeric strings, but they only use capital letters between A-F. Our current controller actions will accept any alphanumeric string as an ID, but Mongo throws an error if we send an incompatible string for an id. We could copy+paste some code into every action to make sure the IDs are valid, but it would probably be better to do that in a separate function. In order to keep your code organized, the cleanest place to put this is in the helpers
object.
TasksController =
options: {}
routes: #Removed to save space
actions: #Removed to save space
helpers:
isValidID: (id) ->
id.match /^[0-9a-fA-F]{24}$/
The isValidID
helper takes an id as a parameter, compares it to a RegExp written to match MongoDB ObjectIDs, and returns a boolean of their match state. When you're in a controller, this
refers to the controller, so the helpers can be accessed in this.helpers
. So isValidID
is access by this.helpers.isValidID(id);
. We will need to update each of the actions that reference a task by ID with this helper.
getTask: (req, res, params) ->
if @helpers.isValidID params.identifier
Tasks = mongoose.model "Tasks"
Tasks.getById params.identifier, (err, task) =>
if err
console.log err
@responses.internalError res
else
if task
@responses.respond res, task
else
@responses.notAvailable res
else
@responses.notAvailable res
I've updated the getTask
action so that it will continue the normal code flow if the ObjectID is valid, otherwise it will send a notAvailable
response. The changes to the rest of the actions will be identical to this one, so I will leave them out. You can view the updated actions in the repository.
It's finished! If you run your project using coffee index
, you will have a fully functioning ToDo list API. I've written some cURL commands you can use to test the API.
#To create a new task
curl -X POST -d '{"name":"Create Example Project","category":"Simple Blog Post"}'
#To update an existing task (get the ID from the GET endpoints)
curl -X PUT -d '{"name":"Create An Example Project"}' http://localhost:3333/api/v0/tasks/52347045a543628e13000001
#To complete an existing task (get the ID from the GET endpoints)
curl -X PUT http://localhost:3333/api/v0/tasks/52347193bf6400d713000003/complete
#To delete an existing task (get the ID from the GET endpoints)
curl -X DELETE http://localhost:3333/api/v0/tasks/52347154bf6400d713000001
And here are the important GET URLs:
Get all tasks: http://localhost:3333/api/v0/tasks
Get a single task by ID: http://localhost:3333/api/v0/tasks/52347154bf6400d713000001
Get all tasks in a category: http://localhost:333/api/v0/tasks/category/somecategory
I hope you've enjoyed learning how to use Simple API. As I've mentioned a few times here and many times in the docs, Simple API is a very new library. It is being used in production in a few places, but it is far from done. I would greatly appreciate feedback, bug reports, new ideas, or any comments. Simple API will evolve with its users, so feel free to tell me how you would like it to work!