Easy API Scaffolding with Simple-API and Node.js

By Joseph Wegner

API’s are often 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 API’s 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 easy to use 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. In this article, I hope 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.

Note: The examples will all be written in CoffeeScript, but can easily be compiled to Javascript.

Setup

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 as well, 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 “https://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. A lower number means 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.

Controllers

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, in this example, 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).

Routes

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 simplest type of route. 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. If you prefer, 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 for this example.

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 https://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 methodsPOST 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"]

Actions

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: reqres, 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: internalErrornotAuthnotAvailableredirect, and respond. Each of them requires res as its 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 parameter 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. In that action, you can see 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.

Helpers

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 ObjectID’s, 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. Therefore, isValidID can be accessed 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.

Where to Go From Here

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"}' https://localhost:3333/api/v0/tasks/52347045a543628e13000001

#To complete an existing task (get the ID from the GET endpoints)
curl -X PUT https://localhost:3333/api/v0/tasks/52347193bf6400d713000003/complete

#To delete an existing task (get the ID from the GET endpoints)
curl -X DELETE  https://localhost:3333/api/v0/tasks/52347154bf6400d713000001

Here are the important GET URLs:

  • Get all tasks: https://localhost:3333/api/v0/tasks
  • Get a single task by ID: https://localhost:3333/api/v0/tasks/52347154bf6400d713000001
  • Get all tasks in a category: https://localhost:333/api/v0/tasks/category/somecategory

I hope that 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 comments that you may have. Simple API will evolve with its users, so feel free to tell me how you would like it to work!

Previous

Creating a Realistic Rain Effect with Canvas and JavaScript

Automating Complex Workflows with Grunt Custom Tasks

Next

3 thoughts on “Easy API Scaffolding with Simple-API and Node.js”

  1. Hi, when I:
    curl -X POST -d ‘{“name”:”Create Example Project”,”category”:”Simple Blog Post”}’
    I get the error:
    curl: no URL specified!
    curl: try ‘curl –help’ or ‘curl –manual’ for more information

Comments are closed.