HTTP Basics

HTTP or hypertext transfer protocol allows transferring data across the web. It is a stateless request & response text-based protocol using a client-server communications model.

  • Request Types, Methods or Verbs
    • GET – Retrieve a resource
    • POST – Create a resource
    • PUT – Update a resource/ (generally) replace a resource with a new resource
    • PATCH – Update a resource/ (generally) allow partial resource body update
    • DELETE – Delete a resource
    • HEAD – The same as GET but only return headers and not content
    • OPTIONS – Get the options for the resource
    • TRACE – Performs message loop-back
  • Optional Headers
    • Headers are used to convey additional information between the client and the server
    • They consist of a header name + colon + header value, e.g., connection: keep-alive
    • Common headers – Request, Response, Connection, Host, User-Agent, Accept Request
  • Status Codes
    • Info
      • 1xx, e.g., 100 – Continue, 101 – Switching Protocols, 102 – Processing
    • Success
      • 2xx, e.g., 200 – OK, 201 – Created, 202 Accepted, 204 – No Content
    • Multiple Choice
      • 3xx, e.g., 300 – Multiple Choices, 302 – Found, 307 – Temporary Redirect
    • Client Error
      • 4xx, e.g., 401 – Unauthorized, 403 – Forbidden, 404 – Not Found, 411 – Length Required
    • Server Error
      • 5xx, e.g., 500-Internal Server Error, 501 – Not Implemented, 502 – Bad Gateway

REST, API, JSON and XHR

  • REST
    • Representational (RE) State(S) Transfer (T)
    • It is a software architectural style that was created to guide the design and development of the architecture for the World Wide Web.
  • API
    • Application (A) Programming (P) Interface (I)
    • It is a contract between an information provider and an information user—establishing the content required from the consumer (the call) and the content required by the producer (the response). 
  • JSON
    • JavaScript (JS) Object (O) Notation (N)
  • XHR
    • XML (X) HTTP (H) Request (R) – a JavaScript API to create AJAX requests. Its methods provide the ability to send network requests between the browser and a server.

Key Constraints for REST

  • Client-Server
    • Separation of concerns – clients, servers, and resources, with requests managed through HTTP.
  • Stateless
    • No client information is stored between get requests, and each request is separate and unconnected.
  • Cacheable
    • Cacheable data that streamlines client-server interactions.
  • Layered
    • The ability to leverage load balancers (LB), proxies (PXY), and firewalls (FW).
  • Code on demand
    • The ability to send executable code to the client when requested, extending client functionality. 
  • Uniform interfaces
    • Resource identification in requests
    • Resource manipulation through representations
    • Self-descriptive messages
    • Hypermedia as the engine of application state (HATEOAS

Node.js & Express

Node.js is an open-source, cross-platform, back-end JavaScript runtime environment that runs on the V8 engine (Google Chrome) and executes JavaScript outside a web browser. A node app runs in a single process, on a single thread. Node.js provides a set of asynchronous I/O primitives in its standard library that prevent JavaScript code from blocking. Generally, libraries in Node.js are written using non-blocking paradigms, making blocking behaviour the exception rather than the norm. Express is a fast, unopinionated, minimalist web framework.

Sample (CRUD) API with Express

Create (C) Read (R) Update (U) Delete (D) API based on a JSON file acting as a persistence layer, with simple routing and cross-origin resource sharing support.

mkdir my_api                  
cd my_api

mkdir html                    # static content
mkdir repos                   # file access module
mkdir assets                  # sample json data

npm init

npm install express --save
npm install nodemon           # monitor and restart node on code change
npm install cors              # enable cors support
npm install --save helmet     # disable X-Powered-By header

Update Package.json config for a more effortless development experience.

"start": "nodemon index.js",

index.js implementation using express and custom data repository.

const express = require('express');
const helmet = require('helmet');

let app = express();
let router = express.Router();
let cors = require('cors');
let dataRepo = require('./repos/dataRepo');

app.use(express.json());
app.use(cors());
app.use(helmet());

router.get('/', function(req, res, next){
    dataRepo.get(function(data){
        res.status(200).json({
            "status": 200,
            "statusTest": "OK",
            "data": data
        });
    },function(err) {
        next(err);
    });    
});

router.get('/search', function(req, res, next){
   let searchObject = {
       "id" : req.query.id,
       "name" : req.query.name
   };
   dataRepo.search(searchObject, function(data){
       res.status(200).json({
           "status" : 200,
           "statusText" : "OK",
           "data": data
       });
     },function(err){
           next(err);
       }); 
});

router.get('/:id', function(req, res, next){
    dataRepo.getById(req.params.id, function(data){
         if(data){
             res.status(200).json({
                "status": 200,
                "statusTest": "OK",
                "data": data
             });
            }
            else {
                res.status(404).json({
                    "status": 404,
                    "statusTest": "Not Found",
                    "message": "Item " + req.params.id + " not found",
                    "error": {
                        "code" : "NOT_FOUND"
                    }
                });
             }
         }, function(err){
            next(err);
        }
    );    
});

router.post('/', function(req, res, next){
    dataRepo.insert(req.body, function(data){
        res.status(201).json({
            "status": 201,
            "statusText": "Created",
            "data": data
        });
    }, function(err){
        if(err){
            next(err);
        }
    });
});

router.put('/:id', function(req, res, next){
    dataRepo.getById(req.params.id, function(data){
        if(data){
            fruitRepo.update(req.body, req.params.id, function(data){
                res.status(200).json({
                    "status":200,
                    "statusTest": "OK",
                    "data" : data
                });
            });
        } else{
            res.status(404).json({
                "status": 404,
                "statusText": "Not Found",
                "message" : "Item " + req.params.id + " not found",
                "error": {
                    "code": "NOT_FOUND",
                    "message" : "Item " + req.params.id + " not found",
                }
            });
        }
    });
});

router.delete('/:id', function(req, res, next){
    dataRepo.getById(req.params.id, function(data){
        if(data){
            fruitRepo.delete(req.params.id, function(data){
                res.status(200).json({
                    "status":200,
                    "statusTest": "OK",
                    "data" : "Item " + req.params.id + " deleted"
                })
            });
        } else{
            res.status(404).json({
                "status": 404,
                "statusText": "Not Found",
                "message" : "Item " + req.params.id + " not found",
                "error": {
                    "code": "NOT_FOUND",
                    "message" : "Item " + req.params.id + " not found",
                }
            });
        }
    });
});

router.patch('/:id', function (req, res, next) {
    dataRepo.getById(req.params.id, function (data) {
      if (data) {
        itemRepo.update(req.body, req.params.id, function (data) {
          res.status(200).json({
            "status": 200,
            "statusText": "OK",
            "message": "Item '" + req.params.id + "' patched.",
            "data": data
          });
        });
      }
      else {
        res.status(404).send({
          "status": 404,
          "statusText": "Not Found",
          "message": "Item '" + req.params.id + "' could not be found.",
          "error": {
            "code": "NOT_FOUND",
            "message": "Item '" + req.params.id + "' could not be found."
          }
        });
      }
    }, function (err) {
      next(err);
    });
});

app.use('/api/', router);

var server = app.listen(5000, function(){
    console.log('Node running on http://localhost:5000');
});

Example data repository (dataRepo) implementation using Node’s file system module.

let fs = require('fs');

const FILE_NAME = './assets/data.json';

let dataRepo = {
    get: function(resolve, reject) {
        fs.readFile(FILE_NAME, function(err, data){
            if(err){
                reject(err);
            } else{
                resolve(JSON.parse(data))
            }
        });
    },

    getById: function(id, resolve, reject) {
        fs.readFile(FILE_NAME, function(err, data){
            if(err){
                reject(err);
            } else{
                item = JSON.parse(data).find(i=> i.id == id);
                resolve(item);
            }
        });
    },

    search: function(searchObject, resolve, reject){
        fs.readFile(FILE_NAME, function (err, data){
            if(err){
                reject(err);
            }else{
                let items = JSON.parse(data);
                if(searchObject){
                    results = items.filter(
                        i=> (searchObject.id ? i.id == searchObject.id : true) &&
                        (searchObject.name ? i.name.toLowerCase().indexOf(searchObject.name.toLowerCase()) >= 0 : true))
                    resolve(results);
                }
            }
        });
    },

    insert: function(newData, resolve, reject){
        fs.readFile(FILE_NAME, function(err, data){
            if(err){
                reject(err);
            }else{
                let items = JSON.parse(data);
                items.push(newData);
                fs.writeFile(FILE_NAME, JSON.stringify(items), function(err){
                    if(err){
                        reject(err);
                    } else{
                        resolve(newData);
                    }
                });
            }
        });
    },

    update: function(newData, id, resolve, reject){
        fs.readFile(FILE_NAME, function(err, data){
            if(err){
                reject(err);
            }else{
                let items = JSON.parse(data);
                let item = items.find(i=> i.id == id);
                if(item){
                    Object.assign(item, newData);
                    fs.writeFile(FILE_NAME, JSON.stringify(items), function(err){
                        if(err){
                            reject(err);
                        }else{
                            resolve(newData);
                        }
                    });
                }
            }
        });
    },

    delete: function(id, resolve, reject){
        fs.readFile(FILE_NAME, function(err, data){
            if(err){
                reject(err);
            }else{
                let items = JSON.parse(data);
                let index = items.findIndex(i=>i.id == id);
                if(index != -1){
                    items.slice(index, 1);
                    fs.writeFile(FILE_NAME, JSON.stringify(items), function(err, index){
                        if(err){
                            reject(err);
                        }else{
                            resolve(index);
                        }
                    });
                } 
            }
        });
    } 
};

module.exports = dataRepo;

Consumer HTML using XHR object and the API.

<!DOCTYPE html>
<html>
<head>
  <title>Sample</title>
</head>
<body>
  <h1>Sample</h1>
  <button onclick="getAllItems();">Get All Items</button>
  <script>
    'use strict';
    const URL = "http://localhost:5000/api/";
    function getAllItems() {
      console.log("GET All");
      let req = new XMLHttpRequest();
      req.onreadystatechange = function () {
        if (this.readyState === XMLHttpRequest.DONE && this.status === 200) {
          let response = JSON.parse(this.response);
          console.log(response.status);
          console.log(response.statusText);
          console.log(response.data);
        }
      };
      req.open("GET", URL);
      req.send();
    }
  </script>
</body>
</html>

To run the API locally on http://localhost:5000/, execute the following:

npm start     # run the 'start' script as per config in 'package.json
# OR
node start    # default, execute the 'server.js' file in 'node' when no config

Then, open index.html to test the CORS example, then test the API in Postman.