21

Serverless Express.js with Now 2

 5 years ago
source link: https://www.tuicool.com/articles/hit/ym22auJ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

This post will teach you how to use Express.js effectively with serverless . Instead of shipping one monolith server, we will break down our APIs into small Express.js lambdas .

Traditionally, Express has been used as a server solution, providing routing and middleware functionality. SinceNow v2 allows us to configure routes to our serverless lambdas, we need not concern Express with routing, but only middleware. In this example, we will make use of a number of middlewares that help us meet our objective of building an application that:

  • Logs a user in via Twitter
  • Gets a user's Twitter handle and picture
  • Applies the DeepDream net on the image
  • Keeps the user logged in for repeated visits
  • Allows the user to log out at a later state

Before we dive in, let us outline some benefits of Serverless Express.

When an application is managed by a monolithic instance of Express, there is a single point of failure of the entire system : if the Express process dies, the system dies . In a serverless architecture, we have distributed points of failure .

In our example, each route is a self-contained, encapsulated instance of Express . Because of this, if the route /user-zone fails, the other routes continue faithfully serving users. This is a significant aspect of the beauty of serverless architectures.
Since each route works with its own instance of Express , it can handle its own specific responsibilities. Our /login route includes passport.js for authenticating with Twitter, while other routes do not.

This separation allows each instance of Express, separated by route, to require its own modules and logic, which in turn, reduces error surface, helps debuggability, and leads to our third point,

The /user-zone route has significantly more sophisticated markup. It could prove tedious to send such markup as a JavaScript string to res.end . Instead, we have decided, only for this route , to use a templating engine:
// /routes/user-zone/index.js
app.set('view engine', 'ejs')
app.set('views', join(__dirname, '../../views'))
app.engine('.ejs', require('ejs').__express)

A templating engine used only in a single route

We later res.render('userZone') and our template is magically rendered, serverless style. This could lead to an impressive amount of savings, since we only include an entire templating engine on the routes that need it, while resorting to simpler markup via string literals on simpler routes ( /routes/index.js ).

The smaller size would in turn lead to a faster bootup and lower costs. Serverless provides this flexibility.

The Dreamifier : An Application Built with Serverless Express.js

Our example application starts on an index route that checks for the existence of a logged in user. This happens via a cookie that is set after a user signs in with Twitter, using cookie-session . We use passport.js to log a user in and then set the cookie.

Since we have some common middleware in our Express app, we configure it once , and then pass the configured app around to other modules:

// /middlewares/common.js
const cookieSession = require('cookie-session')

module.exports = [
  cookieSession({
    name: 'user-from-twitter',
    keys: [process.env.COOK_KEY],
    domain: 'serverless-express.now.sh',

    // Cookie Options
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  })

  /*
    Other middlewares can go here, like
    bodyParser(),
    cookieParser(),
    ...
*/
]

Constructing our chain of common middlewares

// /util/app.js
const app = require('express')()

const commonMiddlewares = require('../middlewares')

app.set('trust proxy', 1)
app.use(...commonMiddlewares)

module.exports = app

Using common middlewares throughout an exported app that is used across our routes

Since our entire app needs to be aware of a user session persisted using cookie-session , we share this middleware across all of our routes. Other middlewares, like passport.js are used only in the routes that require them: allowing for a proper separation of concerns, and lower costs across lambdas that do not require such functionality.
Using a TwitterStrategy from passport-twitter , we simply tell our application to use passport's authenticator first , and then we process our user and persist their info into a cookie:
// We need these.
const passport = require('passport')
const TwitterStrategy = require('passport-twitter').Strategy

// Our configured app.
const app = require('../../util/app')

// Let us add some passport middleware.
app.use(passport.initialize())
app.use(passport.session())

// Use the TwitterStrategy.
passport.use(
  new TwitterStrategy(
    {
      consumerKey: process.env.TWITTER_TOKEN,
      consumerSecret: process.env.TWITTER_SECRET,
      callbackURL: 'https://serverless-express.now.sh/login'
    },
    function(token, tokenSecret, profile, cb) {
      return cb(null, profile)
    }
  )
)

/*
    The next 2 functions are required to be
    defined by passport. Let us not worry about
    them for now.
*/
passport.serializeUser(function(user, done) {
  done(null, user)
})

passport.deserializeUser(function(user, done) {
  done(null, user)
})

/*
    This is where the magic happens: we use Express
    solely for MIDDLEWARE, and not for ROUTING since
    routing is handled by Now 2.
*/

// On ANY (*) GET request, we authenticate, and then:
app.get('*', passport.authenticate('twitter'), (req, res) => {
  // When twitter calls back to this page,
  // 1) Clean up the session.
  delete req.session.passport // This adds a lot of bloat to the cookie and causes it to NOT get persisted.

  // 2) Get user info.
  const { name, screen_name, profile_image_url_https } = req.user._json

  // 3) Set it in req.session, which cookie-session persists in a cookie.
  // this  will allow a user to remain logged in until either
  // the cookie expires, or they log out.
  req.session['user-from-twitter'] = {
    name,
    screen_name,
    profile_image_url_https
  }

  // Take the user to their area.
  res.redirect('/user-zone')
})

// Simple, no? Then we export `app` as our Lambda.
module.exports = app

// :tada:

An Express Lambda that implements Authentication with Twitter

And just like that, we have an application that is able to log a user in via Twitter, and keep them logged in until they explicity logout (their decision), or the cookie expires (our decision).

How do we implement a logout lambda? Like so:

// Our configured app that has cookie-session.
const app = require('../../util/app')

// A basic <html> string.
const baseTemplate = require('../../util/baseTemplate')

// For ANY (*) GET request
// Now handles ROUTING, Express handles MIDDLEWARE
app.get('*', (req, res) => {
  // A header would be nice
  res.append('content-type', 'text/html')

  // Get some user stuff before,
  const { screen_name, profile_image_url_https } = req.session[
    'user-from-twitter'
  ]

  // Remove the session: cookie-session (from our imported app.js)
  // will handle the rest.
  req.session = null

  // Tell the user they have been logged out
  res.end(
    baseTemplate(
      `<div class="container">
  <img class="user-image" alt="${screen_name}" src="${profile_image_url_https}" />
  <h1>Success</h1>
  <h2>You have been sucessfully logged out</h2>
  <a class="button" href="/">Back Home</a>
</div>`
    )
  )
})

// Export your `app` as your Lambda.
module.exports = app

// :tada:

An Express Lambda that logs a user out

And now, we have a complete authentication flow from login to logout. Let us examine another concern: templates.

In some cases, having to rely on template literals being passed to res.end can become a little cumbersome. Express has powerful support for templating engines like pug or ejs . Let us see how we can use them in a serverless fashion.
const { join } = require('path')

// Get our preconfigured app.
const app = require('../../util/app')

// Tell it to do more.
app.set('view engine', 'ejs')
app.set('views', join(__dirname, '../../views'))
app.engine('.ejs', require('ejs').__express)

// For ANY (*) GET,
// Now does ROUTING, Express does HANDLING
app.get('*', (req, res) => {
  // If we're not logged in, go home.
  if (!req.session['user-from-twitter']) {
    res.redirect('/')
  }

  // Add a header.
  res.append('content-type', 'text/html')

  // Get user stuff.
  const { screen_name, profile_image_url_https } = req.session[
    'user-from-twitter'
  ]

  // Render the /views/userZone.ejs template.
  res.render('userZone', {
    screen_name,
    avatar: profile_image_url_https
  })
})

// Export your app
module.exports = app

// :tada:

An Express Lambda that uses Express' template engine.

This is all there is to it. One route, with a little extra configuration for templates. If there are many routes that require an app configured with views and other templating values, consider exporting an appWithTemplates once and sharing it across more complex views. The possibilities for healthy abstraction are limitless with serverless Express.
We have one final concern: routing. Each page of our demo app is a serverlesslambda. We define where these lambdas live in now.json at the root of our project. From there, our folder structure becomes our routing structure for our app: we have amajestic monorepo.
The builds key of now.json signals to Now that each file that matches the pattern in src ought to become a lambda served along the same path in the final deployed URL.
{
  "version": 2,
  "builds": [
    { "src": "**/*.js", "use": "@now/node" },
    { "src": "*.css", "use": "@now/static" }
  ]
}

A simplified snippet of the project's now.json to highlight routing. See the full now.json in the repositiory.

We have more content on configuring deployments inour documentation for those interested.

Finally, we run now in our terminal, and we have a fully separated, fully functional application composed of lambdas that use Express.js in a truly serverless fashion, bringing all of its benefits.

In this post, we have looked at how to use Express.js in a serverless environment by breaking a monolithic Express application into smaller "expresses" that implement their own required logic. We have outlined the benefits thereof. Namely,

Additionally, we looked at code snippets that tackle real world concerns and challenges:

  • Authentication.
  • Middleware composition.
  • Templating.
The entire source code for this project is available in our now-examples repo for those interested. We encourage playing with the code and developing a feel for serverless Express.js. Moreover, the demo is readily available, deployed on Now 2.

We truly believe Express.js, currently a boon to JavaScript developers everywhere, blossoms into an even greater blessing when used within a serverless architecture on self-contained lambdas. We look forward to witnessing the incredible ways in which our developer communities will use these technologies together.

Until then, do not forget to share your expanded handle on Twitter!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK