DEV Community

Cover image for Express style API with OpenJS Architect
Paul Chin Jr. for Begin

Posted on

Express style API with OpenJS Architect

Foundations

We'll cover everything you need to build and deploy a RESTfull serverless API with AWS HTTP APIs, Lambda Functions, and CI/CD on Begin.

The following examples are entirely based on Free Code Camp's APIs and Micro-services Certification: Basic Node and Express but with serverless architecture. Including single-responsibility functions, shared middleware, and static hosting with automated deployments through Begin.

We will be building with OpenJS Architect, a serverless deployment framework that focuses on AWS services to build web apps.

Why have I done this?

Serverless is another tool for web developers to develop applications and deploy them to scalable infrastructure. FreeCodeCamp enabled me to take on a whole new career and taught me life fulfilling skills. I wanted to give back and encourage new and old developers to look at cloud functions instead of stateful server processes. In this project, the FCC API is replicated using serverless technologies and deployed with Begin CI/CD. Requirements include Node.js and a Github account. No AWS Account is needed because we will deploy with Begin CI/CD.

Clone repo and local development

The first step is to click the button to deploy this app to live infrastructure with Begin.

Deploy to Begin

Underneath, Begin will create a new GitHub repo to your account that you can clone to work on locally. Each push to your default branch will trigger a new build and deploy to the staging environment. Your CI/CD is already complete!!

When your app deploys, clone the repo, and install the dependencies.

git clone https://github.com/username/begin-app-project-name.git
cd begin-app-project-name
npm install
Enter fullscreen mode Exit fullscreen mode

Project structure

Your source code is primarily in /src. Each HTTP function represents a discrete endpoint with self-contained logic. For example, get-index contains the response code of a GET request to the root of your application. Static assets and items that would usually be behind a CDN are in /public. The app.arc file is a manifest that describes your source code and the resulting AWS infrastructure. /src and /public are the only folders that get deployed.

fcc-serverless-api
├── public
│   └── default.css
│   └── me_begin.jpg
└── src
   └── HTTP
      └── get-index
         └── index.js
         └── package.json
Enter fullscreen mode Exit fullscreen mode

Function logs and the Node console

console.log('got here') is probably my most used debugging tool. It's a simple way to walk through your code execution. To view logs in Begin, go to your Begin console and inspect the route you want. When your function is invoked, it behaves as if it is being run for the first time. This is different from a regular Express server that is assumed to be long-living and can retain data between route invocations.

If you are eager to skip ahead to sessions and data persistence, check out https://learn.begin.com

Let's look at a console.log() statement to the get-index function.

// src/http/get-index/index.js

let body = `
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <title>Hello HTML</title>
    <link rel="stylesheet" href="_static/default.css">
  </head>
  <body>
    <h1>Hello Beginners!</h1>
    <img src="_static/me_begin.jpg">
    <p>Oh yea, wait a minute</p>
     <form action="/name" method="post">
      <label>First Name :</label>
      <input type="text" name="first" value="Mr."><br>
      <label>Last Name :</label>
      <input type="text" name="last" value="POSTman"><br><br>
      <input type="submit" value="Submit">
    </form>
  </body>
</html>
`
// main Lambda function handler, returns an HTTP response with an HTML string in the body.
exports.handler = async function http(req) {
  console.log('Praise Cage')
  return {
    statusCode: 200,
    headers: {
      'content-type': 'text/html; charset=utf8',
      'cache-control': 'no-cache, no-store, must-revalidate, max-age=0, s-maxage=0'
    },
    body
  }
}
Enter fullscreen mode Exit fullscreen mode

Now when you visit your index route from your staging link, you should see the console output:

Alt Text

Serverless HTML and static assets

In the FCC Express challenge, they show you how to create a web server by opening a port to listen on. With serverless functions, you don't need to create that layer of code. HTTP requests are handled by AWS API Gateway, a service that will act as part of your web server. When users make a request, each route is handled by a Lambda Function. This gives us the ability to only write logic that pertains to the request and response needed by a single route. It also has added security because the control of that function is only allowed by your app on your Begin domain. Architect takes care of IAM roles and service permissions when your code is deployed.

The combination of code and the underlying infrastructure is called "Infrastructure as Code". We achieve this by writing a manifest called app.arc in the root of the project. Architect captures cloud resources and associated function code in a single file.

So let's take a look at it now.

# app.arc
@app
fcc-apis   # app namespace - this helps organize the backend resources

@static    # declaration of static assets, defaults to the /public folder

@http      # declaration of HTTP routes, each route has it's own function handler organized by folder
get /      # the function handler is found in /src/http/get-index/index.js
Enter fullscreen mode Exit fullscreen mode

Each function is self contained in it's own function folder according to route and HTTP method. One failing function won't take down the entire app, just the code behind that route.

To start serving HTML and static assets, we can put them into the /public folder. Notice that the image served from /public is referenced with _static. Take a look at line 13, <img src="_static/me_begin.jpg">.

Serve JSON on a specific route

The heart of a REST API is specifying some action with a URL path, and an HTTP method. The method is defined by app.arc, which tells API Gateway how to interpret the HTTP request on a route. That path could return JSON data, an HTML string, or any other kind of text. In this section, we want to return JSON at the route /json. Setting it up means adding this route to app.arc and writing a get-json handler function.

# app.arc
@http
get /json
Enter fullscreen mode Exit fullscreen mode
// src/http/get-json/index.js
exports.handler = async function http (req) {
  let message = "Praise Cage!"
  return {
    statusCode: 200,
    headers: {
      "content-type": "application/json; charset=utf-8"
    },
    body: JSON.stringify({"message": message})
  }
}
Enter fullscreen mode Exit fullscreen mode

Environment Variables

Environment variables are values that can be used during runtime. We typically hold sensitive information like API keys and configuration secrets that should not be stored in .git. In order to use environment variables with Sandbox, our development server, we need to create a .arc-env file. Then we can add staging and production environment variables in the Begin Console.

# .arc-env
@testing
MESSAGE_STYLE uppercase
Enter fullscreen mode Exit fullscreen mode

Refactor get-json to check for the environment variable MESSAGE_STATUS

// src/http/get-json/index.js
exports.handler = async function http (req) {
  let message = "Hello json"

  // new code to check for environment variable
  if (process.env.MESSAGE_STYLE==="uppercase") {
    message = message.toUpperCase()
  }

  return {
    statusCode: 200
    headers: {
      "content-type": "application/json; charset=utf-8"
    },
    body: JSON.stringify({"message": message})
  }
}
Enter fullscreen mode Exit fullscreen mode

Add the environment variable in the Begin Console by navigating to "Environments", typing in your key and value, and clicking add. Note that there are different areas for staging and production.

Alt Text

Root-level request logger and middleware

In order to create a logger on every request, we can use a special folder called src/shared to create utilities that multiple functions can access. Since each function is isolated, Architect will copy everything in src/shared into the node_modules folder of every function. We will start with declaring a new route, writing a handler function, and writing a logger utility function.

# app.arc
@http
get /now
Enter fullscreen mode Exit fullscreen mode
// src/shared/utils.js
function logger(req){
  // takes a request and logs the HTTP method, path, and originating public IP address.
  console.log(`${req.httpMethod} ${req.path} - ${req.headers['X-Forwarded-For']}`)
  return
}

module.exports = logger
Enter fullscreen mode Exit fullscreen mode

Now you can add logger() to any function you want by requiring it at the top. We can combine the shared code with an Express style middleware in @architect/functions to complete the next challenge.

cd src/http/get-now/
npm init -y
npm install @architect/functions
Enter fullscreen mode Exit fullscreen mode
// src/http/get-now/index.js

// require logger and @architect/functions
let logger = require('@architect/shared/utils')
let arc = require('@architect/functions')

// first function call to modify the req object
function time(req, res, next) {
  req.time = new Date().toString()
  next()
}

// response function with response object
function http(req, res) {
  let time = `Praise Cage! The time is: ${req.time}`
  res({
    "json": {time: time}
  })
}

// arc.http registers multiple functions and executes them in order
exports.handler = arc.http(time, http)
Enter fullscreen mode Exit fullscreen mode

arc.http registers multiple functions. Each function will get executed to modify the req object. If a function does not end the request/response cycle, it must call next() and the final function must call res()

To learn more about the arc.http request and response methods, check out https://arc.codes/reference/functions/http/node/classic.

Get route(path) parameter input from the client

In this function, we will build an echo endpoint to respond with a JSON object of the word that is passed in as a request parameter. Add a new endpoint to app.arc and write a corresponding handler function.

# app.arc
@http
get /echo/:word
Enter fullscreen mode Exit fullscreen mode
// src/http/get-echo-000word/index.js
exports.handler = async function http(req){
  let { word } = req.pathParameters
  return {
    statusCode: 200,
    headers: {
      'content-type':'application/json; charset=utf-8'
    },
    body: JSON.stringify({ echo: word})
  }
}
Enter fullscreen mode Exit fullscreen mode

A GET request to /echo/freecodecamp, will result in a request object that has a property pathParameters with the object { word: 'freecodecamp'} as a value. This is useful for dynamic routes like users or postId where the route can be appended with any string that you can catch and reference.

Get query parameter input from the client

Another way to pass data to your API endpoint uses query parameters. We're going to add a get-name HTTP route with a corresponding handler.

# app.arc
@http
get /name
Enter fullscreen mode Exit fullscreen mode
// src/http/get-name/index.js
exports.handler = async function http(req, res) {
  let { first, last } = req.queryStringParameters
  return {
    statusCode: 200,
    headers: {
      'content-type':'application/json; charset=utf-8'
    },
    body: JSON.stringify({
      "name": `${first} ${last}`
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

A GET request to /name?first=nic&last=cage, will result in a request object that has a property queryStringParameters with the object { first: 'nic', last: 'cage' } as a value. We can treat this similarly to route parameters. A query parameter can give the backend certain keys to filter or sort items.

Parse request bodies and data from POST requests

Another way to receive data is from a POST request as an HTML form. HTML forms allow the browser to submit data to the server-side without using JavaScript. The data is part of the HTTP payload in the request body. In this example, we are using urlencoded body. Architect uses Base64 encoded strings for all request bodies, and we have a helper method in @architect/functions to help parse request bodies. Since each function is isolated, we will have to install and manage dependencies per function folder.

But first, let's set up a post-name function and route.

# app.arc
@http
post /name
Enter fullscreen mode Exit fullscreen mode

Then we can install @architect/functions for the body parser.

cd src/http/post-name
npm init -y
npm install @architect/functions
Enter fullscreen mode Exit fullscreen mode

Now let's write the function handler

// src/http/post-name
let arc = require('@architect/functions')

exports.handler = async function http(req) {
  let {first, last} = arc.http.helpers.bodyParser(req)
  return {
    statusCode: 200,
    headers: {"Content-type": "application/json; charset=UTF-8"},
    body: JSON.stringify({
      name: `${first} ${last}`
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Now you can use index.html to submit a form with any name you would like, i.e. Nic Cage, and the post-name handler with reply with { "name": "Nic Cage"}.

Infrastructure as Code

This is a serverless approach to building a REST API and serving static assets. Take a look at your final app.arc file, and you will see an entire rundown of your entire app.

# app.arc
@app
fcc-apis

@static

@http
get /             # root proxy to static assets
get /json         # deliver JSON data
get /now          # middleware example
get /echo/:word   # get path parameters
get /name         # get query string parameters
post /name        # process HTML Form data
Enter fullscreen mode Exit fullscreen mode

Each commit to your default .git branch triggers a deploy to staging on Begin. When you are ready for production, click Deploy to Production in your Begin Console and say "Hello" to Ship-it Squirrel.

For extra funsies, you can see the original FCC Express app with all the same capabilities, running in a single giant Lambda function. You can see that code here: https://github.com/pchinjr/boilerplate-express

Top comments (5)

Collapse
 
rennygalindez profile image
Renny Galindez

Excelente, gracias!
Una muy buena Introducción!

Collapse
 
pchinjr profile image
Paul Chin Jr.

You're welcome! Glad to hear that it helps, if you have any questions, let me know. I have other tutorials about working with APIs and serverless functions.

Collapse
 
rennygalindez profile image
Renny Galindez

At this moment I have one doubt, My functions works isolate and we have to install packages separately for each function, doing this does not result is redundant?, I say these because if we need a package in many function we have to install it as many times as functions we have and our project will have one pacakage in multpiple folders, maybe these is not important, it's just a coruosity for me!

Thread Thread
 
pchinjr profile image
Paul Chin Jr.

When the functions are deployed, they are separate and require their own dependencies. This is good for isolation, but the dev experience has changed since I wrote this article. Now when you use Architect, you no longer need a per function package.json. The Architect framework takes care of looking it up for you. We're calling it Lambda treeshaking blog.begin.com/architect-8-4-say-h...

Thread Thread
 
rennygalindez profile image
Renny Galindez

Thaks! I begining with serverless and you answers help me a lot!