rebound-api

An Express middleware for elegant API creation

Usage no npm install needed!

<script type="module">
  import reboundApi from 'https://cdn.skypack.dev/rebound-api';
</script>

README

Rebound Logo

An Express Middleware for Elegant API Creation

- - - #### Wait, what is this again? Express is a powerful tool for building Node servers. However, for better or worse, it is very un-opinionated and it can difficult to know how best to organize your API services. This module enables you to use your file system to declare your API endpoints.

Hey! This module has been spun off into its own project from the Rebound Seed Project and can be used as a standalone library for API creation. Feel free to use this with or without the rest of Rebound, though we definately recommend checking it out!

How To Use

- - - Simply: ``` Shell $ npm install --save rebound-api ```

Then, in your app.js file:

// Get our dependancies
  var express = require('express');
  var api     = require('rebound-api');
  
// Init Core
  var app = express();
  app.set('port', PORT);
  
  
/******************************************* 
       Additional Middleware Go Here 
*******************************************/
  
  
// Automatically discover API endpoints in `/api` directory. Must be last middleware.
  app.use(api(express));

// Start Server
  http.createServer(app).listen(app.get('port'), function(){
    console.log(('Express server listening on port ' + app.get('port')));
  });

How It Works

- - -

Its quite simple really – the Rebound API middleware checks req.xhr:

If the request is an AJAX request, it will attempt to roue to an API endpoint defined in your project's /api directory and send a JSON response back to the client.

If the request is not an AJAX request, it will respond using a file that you specify (defaults to /index.html).

Important: This middleware will catch all requests – both AJAX and otherwise – so it must be the last middleware in your express server.

·

There are three concepts to understand before coding with the Rebound API middleware

1) API Discovery

When starting, the Rebound API middleware will look for a directory called /api at the root of your project. This directory contains all the files that will define your api (how that works is described below), the paths of which define your public facing API paths.

The project on the left in the below image, will show its corrosponding output when you start your server. This example will be referred to throughout this section:

api_screenshots

API File Paths:

The files paths in your /api directory are the express routes you would normally write at the bottom of your app.js file to handle requests. Here is the express router documentaiton if you need to brush up on how to write routes.

The file and directory names may be any valid string or string pattern used to describe an Express route. For example: /user/:uid?.js, as is shown in the above example, defines a route user that can accept an optional uid (User ID) parameter.

The file name index.js is special. Files named index.js will act as the root route for their parent directory. The donate directory in the above example shows this well. the directory structure:

api
 |--- donate
        |--- history.js
        |--- index.js

Defines two routes: /donate/history and /donate. An equivelent, but far less convenient, structure would be:

api
 |---donate
 |     |--- history.js
 |
 |---donate.js
API Path Specificity:

No more manually managing your route ordering! Your routes are automagically registered with express in order from most to least specific. For instance, above, the user paths are loaded in order from most to least specific: /user/password/recovery > /user/login > /user/:uid?.

API Errors

The Rebound API middleware will display the paths discovered in your console for your debugging pleasure. If there is an error in one of your API files, it will not kill your server. Instead, it will print a nice big, red error for that route along with the error and line number that caused it. Convenient!

2) Writing API Files

We will be using jSend, a simple (and personal favorite) JSON format for sending data back and forth between the server and frontend, in these examples. Feel free to use whatever specification you like best!

The files in your /api folder export the different http methods implemented for this path. The methods implemented for a particular path are printed out next to the registered path in the console, as shown in section 1.

A simple API file may look like this:

// This file implements the `GET` HTTP method, which returns a JSON blob. 
// This JSON is sent back to the client and the response is closed. 
exports.GET = function(req, res){
  return {
    status: 'success',
    data: {
      firstName: 'Luke',
      lastName: 'Skywalker'
    }
};

For you lazy people out there – a tl;dr:

  • These HTTP method implementations are middleware.
  • Different HTTP methods are named exports from your API file. Current valid methods are ALL, GET, POST, UPDATE and DELETE.
  • Like any other middleware, they are passed the req and res objects.
  • These API definitions do not accept a next callback – they are always the last middleware before a response.
  • These middleware should always return either JSON or a Promise.
  • The response value will be sent back to the client.
  • If the response is a Promise, the Rebound API will wait for it to resolve and send its value.
  • If the response JSON has the property code, it will be use as the HTTP status code of the response.
  • If an error occurs in your API call, it will be:
  • Gracefully caught
  • Logged in the console
  • And 500 response will be sent back to the client
  • If no route is found that matches the request, a 400 response is sent back to the client

The full explaination: An API file that only exports a single function will default to the GET http method:

// Same as the example above
module.exports = function(req, res){
  return {
    status: 'success',
    data: {
      firstName: 'Luke',
      lastName: 'Skywalker'
    }
  };
};

An API file that may export multiple HTTP method names:

module.GET = function(req, res){
 // Make a database call or something here
  return {
    status: 'success',
    data: {
      firstName: 'Luke',
      lastName: 'Skywalker'
    }
  };
};

module.POST = function(req, res){
 // Update your database or something here
  return { status: 'success' };
};

An API method implementation may return a Promise. Rebound API will wait for the promise to resolve or reject and sent its resolved or rejected value back to the client. Great for asynchronous database calls:

var Promise = require("bluebird");

module.GET = function(req, res){
 // Make an async database call or something here
  return new Promise(function(resolve, reject){
    resolve({
      status: 'success',
      data: {
        firstName: 'Luke',
        lastName: 'Skywalker'
      }
    });
  });
};

module.POST = function(req, res){
 // Do some async validation or something here
  return new Promise(function(resolve, reject){
    reject({
      status: 'error',
      code: '403',  // If the response has a property `code`, it will be used as the http response code.
      message: 'You are not authorized to post to this endpoint!'
    });
  });
};

3) Calling APIs Server Side

The Rebound API middleware puts an api property on on the res.locals object at the begining of every new request and is accessable to every middleware in your express app. It exposes get, post, put and delete methods which each take a url and optional data object. This allows you to consume your API calls server side to build more powerful services, as well as client side.

A server side call looks like this:

res.locals.api.get('/user/123')
   .then(function(result){
     // You have access to the API result here
   });

An internal API call will always return a Promise, regardless of if the API function returns a Promise itself, or just a JSON blob.

You are able to pass internal API calls an optional JSON object as a second paramater. This object will augment the req.body object on the original request object for the lifetime of the internal API call.

Our API file /api/user/:uid.js

exports.POST = function(req, res){
  // req.body == { firstName: 'Ash', numPkmn: 6 }
  return { status: 'success', data: { yell: 'Gotta Catch 'Em All!' }}
}

Middleware posting to /user/:uid

function(req, res){
  // req.body = { numPkmn: 6 }
  res.locals.api.post('/user/123', { firstName: 'Ash' })
   .then(function(result){
    // You have access to the API result here
    // result.data == { yell: 'Gotta Catch 'Em All!' }
    // req.body == { numPkmn: 6 }
   });
}

The fact that internal APIs always return a promise allows us to do some creative things when drafting other API calls, consuming existing APIs to create new ones.

An API file /api/profile/:uid.js. This endpoint returns an entire profile object. So much info!

exports.GET = function(req, res){
  return { 
    status: 'success', 
    data: { 
      firstName: 'Ash',
      lastName: 'Ketchum',
      numPkmn: 151,
      hometown: 'Pallet Town'
    }
  }
}

An API file /api/miniprofile/:uid.js. This miniprofile API endpoint will only return the firstName and lastName properties of the full profile.

exports.GET = function(req, res){
  res.locals.api.get('/user/' + req.params.uid)
   .then(function(result){
      result.data = {
          firstName: result.data.firstName,
          lastName: result.data.lastName
        };
      return result;
   });
}