A Syntax Comparison of Different Basic Server API implementations

I recently cloned the React tutorial source code to get started with ReactJS. I immediately noticed that FB added 7 (!) different server implementations to support the ‘comments’ example app: NodeJS, Go, Lua, PHP, Python, Ruby and Haskell. All of the bellow server codes implement a GET and a POST for a comments components. They all store the comments as a basic JSON file locally.

As a veteran Java developer, I got hooked to NodeJS a few years ago thanks to it’s simplicity and elegance. I’m hearing a lot of buzz from other general purpose languages such as Go and Lua so I though it would be nice to compare the different implementations side by side (even better if someone else did all the writing heavy lifting). I’m NOT going into a performance comparison, but just the esthetics, ease of reading the code and number of lines to implement the same behaviour.

NodeJS

var fs = require('fs');
var path = require('path');
var express = require('express');
var bodyParser = require('body-parser');
var app = express();

var COMMENTS_FILE = path.join(__dirname, 'comments.json');

app.set('port', (process.env.PORT || 3000));

app.use('/', express.static(path.join(__dirname, 'public')));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));

app.get('/api/comments', function(req, res) {
  fs.readFile(COMMENTS_FILE, function(err, data) {
    res.setHeader('Cache-Control', 'no-cache');
    res.json(JSON.parse(data));
  });
});

app.post('/api/comments', function(req, res) {
  fs.readFile(COMMENTS_FILE, function(err, data) {
    var comments = JSON.parse(data);
    comments.push(req.body);
    fs.writeFile(COMMENTS_FILE, JSON.stringify(comments, null, 4), function(err) {
      res.setHeader('Cache-Control', 'no-cache');
      res.json(comments);
    });
  });
});


app.listen(app.get('port'), function() {
  console.log('Server started: http://localhost:' + app.get('port') + '/');
});

I know I’m biased but even without any fancy promises and other tweaks we have a VERY straight forward and self explanatory code that requires very few comments. The code has non-blocking I/O and seems to be able to support quite a few concurrent requests.

Lines of code: ~35

Go (A.K.A Golang)

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"sync"
)

type comment struct {
	Author string `json:"author"`
	Text   string `json:"text"`
}

const dataFile = "./comments.json"

var commentMutex = new(sync.Mutex)

// Handle comments
func handleComments(w http.ResponseWriter, r *http.Request) {
	// Since multiple requests could come in at once, ensure we have a lock
	// around all file operations
	commentMutex.Lock()
	defer commentMutex.Unlock()

	// Stat the file, so we can find its current permissions
	fi, err := os.Stat(dataFile)
	if err != nil {
		http.Error(w, fmt.Sprintf("Unable to stat the data file (%s): %s", dataFile, err), http.StatusInternalServerError)
		return
	}

	// Read the comments from the file.
	commentData, err := ioutil.ReadFile(dataFile)
	if err != nil {
		http.Error(w, fmt.Sprintf("Unable to read the data file (%s): %s", dataFile, err), http.StatusInternalServerError)
		return
	}

	switch r.Method {
	case "POST":
		// Decode the JSON data
		var comments []comment
		if err := json.Unmarshal(commentData, &comments); err != nil {
			http.Error(w, fmt.Sprintf("Unable to Unmarshal comments from data file (%s): %s", dataFile, err), http.StatusInternalServerError)
			return
		}

		// Add a new comment to the in memory slice of comments
		comments = append(comments, comment{Author: r.FormValue("author"), Text: r.FormValue("text")})

		// Marshal the comments to indented json.
		commentData, err = json.MarshalIndent(comments, "", "    ")
		if err != nil {
			http.Error(w, fmt.Sprintf("Unable to marshal comments to json: %s", err), http.StatusInternalServerError)
			return
		}

		// Write out the comments to the file, preserving permissions
		err := ioutil.WriteFile(dataFile, commentData, fi.Mode())
		if err != nil {
			http.Error(w, fmt.Sprintf("Unable to write comments to data file (%s): %s", dataFile, err), http.StatusInternalServerError)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.Header().Set("Cache-Control", "no-cache")
		io.Copy(w, bytes.NewReader(commentData))

	case "GET":
		w.Header().Set("Content-Type", "application/json")
		w.Header().Set("Cache-Control", "no-cache")
		// stream the contents of the file to the response
		io.Copy(w, bytes.NewReader(commentData))

	default:
		// Don't know the method, so error
		http.Error(w, fmt.Sprintf("Unsupported method: %s", r.Method), http.StatusMethodNotAllowed)
	}
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "3000"
	}
	http.HandleFunc("/api/comments", handleComments)
	http.Handle("/", http.FileServer(http.Dir("./public")))
	log.Println("Server started: http://localhost:" + port)
	log.Fatal(http.ListenAndServe(":"+port, nil))
}

Go seems to be an emerging general purpose language especially where high CPU/Memmory efficiency is required. It looks better than C++ (IMHO) but not much :( . The writers of the language pride themselves that the language only has twenty something keywords but come on - ‘func’ ?! 3 different ways to declare a variable?! Multiple return values?! I’m too old for this…

Seriously - this is a pretty poor approach to keep the code readable, and understandable by other team members. The number of comments in the code indicate that much…

Lines of code: ~95 (~85 without the comments)

Lua


handle("/api/comments", function()

  -- Set the headers
  content("application/javascript")
  setheader("Cache-Control", "no-cache")

  -- Use a JSON file for the comments
  comments = JFile("comments.json")

  -- Handle requests
  if method() == "POST" then
    -- Add the form data table to the JSON document
    comments:add(ToJSON(formdata(), 4))
  end

  -- Return the contents of the JSON file
  print(tostring(comments))

end)

servedir("/", "public")

This example is a bit of a missfit since it’s not a standalone server - it is meant to run in Algernon. It’s extremely short and pretty straight forward. A bit weird to my taste ;)

Ruby

require 'webrick'
require 'json'

port = ENV['PORT'].nil? ? 3000 : ENV['PORT'].to_i

puts "Server started: http://localhost:#{port}/"

root = File.expand_path './public'
server = WEBrick::HTTPServer.new Port: port, DocumentRoot: root

server.mount_proc '/api/comments' do |req, res|
  comments = JSON.parse(File.read('./comments.json', encoding: 'UTF-8'))

  if req.request_method == 'POST'
    # Assume it's well formed
    comment = {}
    req.query.each do |key, value|
      comment[key] = value.force_encoding('UTF-8')
    end
    comments << comment
    File.write(
      './comments.json',
      JSON.pretty_generate(comments, indent: '    '),
      encoding: 'UTF-8'
    )
  end

  # always return json
  res['Content-Type'] = 'application/json'
  res['Cache-Control'] = 'no-cache'
  res.body = JSON.generate(comments)
end

trap('INT') { server.shutdown }

server.start

What can I say… No variable declarations, Bit operators for array concatenation («), no explicit function calls (server.shutdown). All in all it’s pretty simple to read and understand but I just can’t get used to the ‘natural’ syntax.

Lines of code: ~35

PHP

$scriptInvokedFromCli =
    isset($_SERVER['argv'][0]) && $_SERVER['argv'][0] === 'server.php';

if($scriptInvokedFromCli) {
    $port = getenv('PORT');
    if (empty($port)) {
        $port = "3000";
    }

    echo 'starting server on port '. $port . PHP_EOL;
    exec('php -S localhost:'. $port . ' -t public server.php');
} else {
    return routeRequest();
}

function routeRequest()
{
    $comments = file_get_contents('comments.json');
    $uri = $_SERVER['REQUEST_URI'];
    if ($uri == '/') {
        echo file_get_contents('./public/index.html');
    } elseif (preg_match('/\/api\/comments(\?.*)?/', $uri)) {
        if($_SERVER['REQUEST_METHOD'] === 'POST') {
            $commentsDecoded = json_decode($comments, true);
            $commentsDecoded[] = ['author'  => $_POST['author'],
                                  'text'    => $_POST['text']];

            $comments = json_encode($commentsDecoded, JSON_PRETTY_PRINT);
            file_put_contents('comments.json', $comments);
        }
        header('Content-Type: application/json');
        header('Cache-Control: no-cache');
        echo $comments;
    } else {
        return false;
    }
}

Well PHP has an place very high in my list of languages I never want to touch… I still remember the first time I tried to concatenate 2 strings with a plus (+) operator… The hours of frustration… I also hate the excessive usage of Dollar ($) signs everywhere… The code is pretty short but it is not a standalone server and relies on a CGI server so it’s not relevant.

Lines of code: ~36

Python

import json
import os
from flask import Flask, Response, request

app = Flask(__name__, static_url_path='', static_folder='public')
app.add_url_rule('/', 'root', lambda: app.send_static_file('index.html'))

@app.route('/api/comments', methods=['GET', 'POST'])
def comments_handler():

    with open('comments.json', 'r') as file:
        comments = json.loads(file.read())

    if request.method == 'POST':
        comments.append(request.form.to_dict())

        with open('comments.json', 'w') as file:
            file.write(json.dumps(comments, indent=4, separators=(',', ': ')))

    return Response(json.dumps(comments), mimetype='application/json', headers={'Cache-Control': 'no-cache'})

if __name__ == '__main__':
    app.run(port=int(os.environ.get("PORT",3000)))

Indentation based syntax is always pretty tricky - Found myself struggling more than once with scope fixing after accidentally deleting a tab.

Haskell

{-# LANGUAGE OverloadedStrings #-}

module Main (main) where

import Web.Scotty

import Control.Monad (mzero)
import Control.Monad.Trans
import Network.Wai.Middleware.Static
import Network.Wai.Middleware.RequestLogger (logStdoutDev)
import Data.ByteString.Lazy (readFile, writeFile, fromStrict)
import qualified Data.ByteString as BS (readFile)
import Prelude hiding (readFile, writeFile)
import Data.Aeson hiding (json)
import Data.Text
import Data.Maybe (fromJust)

data Comment = Comment {
      commentText :: Text,
      author :: Text
    } deriving (Eq, Show, Ord)

instance FromJSON Comment where
    parseJSON (Object v) = Comment <$>
                           v .: "text" <*>
                           v .: "author"
    parseJSON _ = mzero

instance ToJSON Comment where
     toJSON (Comment ctext author) = object ["text" .= ctext, "author" .= author]


main :: IO ()
main = scotty 3000 $ do

    middleware $ staticPolicy (noDots >-> addBase "public")
    middleware logStdoutDev

    get "/" $ file "./public/index.html"

    get "/api/comments" $ do
      comments <- liftIO $ readFile "comments.json"
      json $ fromJust $ (decode comments :: Maybe [Comment])

    post "/api/comments" $ do
      comments <- liftIO $ BS.readFile "comments.json"
      let jsonComments = fromJust $ (decode $ fromStrict comments :: Maybe [Comment])
      author <- param "author"
      comment <- param "text"
      let allComments = jsonComments ++ [Comment comment author]
      liftIO $ writeFile "comments.json" (encode allComments)
      json allComments

By now I’m totally speechless… Who the f@%ck is Scotty?! What are the randomly set $ signs?! If this is the language of your choice there is little I can do for you…

Conclusion

This is it. Came out to be a syntax trashing post :). Obviously C-like syntax is very easy for me as I spent the better parts of the last 15 years using it and I’m aware of my biased opinions. I mean no offense to anyone that took the time and effort to create different/new programming languages and to anyone using them. I’m just saying that a ‘cool’ and ‘new’ language is not always easier to maintain and understand. No harm in using the function or define keywords instead of the short-hand func and def. Shorter is not necessarily better ;)

Javascript Architect

Frontend Group
Thank you for your interest!

We will contact you as soon as possible.

Send us a message

Oops, something went wrong
Please try again or contact us by email at info@tikalk.com