Zero to Node: Node.js in Production

Goal

Describe how we did a particular project in Node.

The Problem

The Solution

A node.js app.

Yikes!

What's this app really doing?

Getting started

Install is easy. Check nodejs.org for instructions.

$ brew install node

Recommended: Install NVM (Node version manager).

Dependencies

"dependencies": {
  "config": "0.4.15",
  "express": "3.x",
  "underscore": "1.3.3",
  "deep-extend": "0.2.2",
  "validator": "0.4.10",
  "winston": "0.6.2"
},
"devDependencies": {
  "coffee-script": "1.3.3",
  "jshint": "0.7.1",
  "coffeelint": "0.4.0"
}
$ npm install

Modules

require allows importing third-party packages into our code.

require'ing is very simple:

express = require "express"

Modules are just js files.

utils = require "./common/utils"

Dev Environment

// JavaScript               # CoffeeScript

fn = function (args) {      fn = (args) ->
  return args;                args
};

var obj = {                 obj =
  key: value                  key: value
};

Configuration

node-config provides per-environment config.

Sample config:

module.exports =
  app:
    port: 8004
  log:
    level: "debug"
  tts:
    input:
      maxChars: 200

Place config files In config sub-directory:

node-config chooses based on NODE_ENV environment variable.

Express and Connect

Express (HTTP framework) and Connect (middleware) provide flexible flow control:

# Create Express app.
express = require 'express'
app     = express()

# Set up a route.
app.get '/audio', (req, res) ->
  # Process request...
  # When finished, send response:
  res.send 200, responseData

# Add middleware.
middle = (req, res, next) ->
  # Process request...
  # When finished, can short-circuit:
  return res.send 200, responseData if allDone
  # Or can move on to next middleware:
  next()

app.use middle

Structure and Decomposition

Really up to you. Here's our project layout:

/cicero
   |-config    
   |-docs
   |-lib               (<--- compiled JS)
   |  |--index.js
   |  |--tts.js
   |-node_modules ...
   |-src               (<--- Coffee-script)
      |--index.coffee
      |--tts.coffee

Structure and Decomposition

index.coffee

Structure and Decomposition

tts.coffee

Structure and Decomposition

tts.coffee exposes middleware functions with signature (req, res, next):

module.exports =
  parseArgs:   TtsArgs.parse
  searchCache: TtsCache.search
  exec:        TtsExec.exec

Attach middleware to routes in index.coffee:

tts    = require "./tts"
middle = [log, tts.parseArgs, tts.searchCache, tts.exec]
app.get "/audio", middle

Async

Callbacks

First async paradigm.

fs.open path, 'r', (err, file) ->
  # In callback function.

  # Short-circuit and handle error if err is non-null:
  return handleError err if err

  # Otherwise, everything went ok, and we can do 
  # something with file now that it's ready...

Events

Second async paradigam.

# Create our process object.
ttsProcess = spawn 'tts_command', args, options

# Listen for error event.
stderr = ""
ttsProcess.stderr.on "data", (data) ->
  # Accumulate data from stderr.
  stderr += data

# Listen for exit event.
ttsProcess.on "exit", (code) ->
  # Check errors, exit.
  if stderr or code isnt 0
    return new Error(stderr ? "TTS failed")

Streams

Why use streams?

Streams

Stream audio file to HTTP response (res) and S3 cache (s3Stream):

# Get readable stream reference to audio file.
fileStream = fs.createReadStream tmpAudioFile

# res is our HTTP response object, and it's a writeable stream.
# s3Stream is our writeable stream to Amazon S3.
# Let the streaming begin!
fileStream.pipe(res)
fileStream.pipe(s3Stream)

# Listen for error events.
fileStream.on "error", errHandle
s3Stream.on "error", errHandle

# Cleanup on end event.
fileStream.on "end", ->
  # Do cleanup (delete tmpAudioFile)

Logging

Gain visibility into what your app is doing.

Provision and Deploy

Monitoring

Summing Up

Results

Node Challenges

Links

Docs

Community

Coffee-script

Chef

Thanks!

@williamjohnbert

williamjohnbert.com

SpanishDict is hiring! spanishdict.com/careers

/

#