Serverless.

It's kind of a phenomenon. All sorts of web developers can make use of it, including front-end developers! Here's one way to think about it: they allow you to take your front-end skills and do things that typically only a back-end can do. Depending on what you all make use of, serverless is possibly more accurately referred to as Cloud Functions or Functions as a Service.

Here's a rather remarkable thing serverless can make possible on the client: saving and reading things from a database. That's right! It's possible to have a database and deal with it entirely through front-end code. Technically, the database still exists on a server, so the word serverless can feel a little disingenuous, but it's not a server that you need to buy and build and maintain and deal with directly.

Why bother with doing a database this way? For one thing, it means you can host the rest of your site much more easily. Wanna build a React-powered blog on GitHub pages? You totally can! Wanna use the super fast static file host Netlify to build a Vue-based community-driven recipe site? Do it.

I bet where you can see this is going: serverless opens up tons of possibilities for what you can build right here on CodePen! Let's dig into this and build ourselves a fully functional blog.

That's right, even though CodePen itself doesn't have data storage, you could use serverless technology to handle all that.

COOL RIGHT?! I sure think so. Let's do this.

Table of Contents

  1. Tools of the Trade
  2. Set Up a Firebase Project
  3. Set Up Cloud Functions
  4. API development: List All Posts
  5. Setting up the Front End
  6. Single Posts (and improving our API)
  7. Creating New Blog Posts
  8. Handle URL Changes (and cleaning up)
  9. Keep Going!

Just looking for the final product and code?

Tools of the Trade

From here on out, we're going to assume you know the basics of the command line and have npm installed on your machine.

There are lots of possible tools in the world of serverless, but for this demo, in addition to building the front end on CodePen, we'll be doing the serverless stuff on Firebase, so you'll need a Google account.

Step 1) Set up a Firebase project

Let's create a project and then import some placeholder data (literally: some fake blog posts) that we can use initially as we develop our API.

  1. Login to your Google account and navigate to your Firebase console

  2. Select the big Add Project

  3. Create the project by providing a project name. The project ID is provided for you, but you can edit it, if you'd like. The name is just for finding your project again in your list of projects, but the ID is used in your actual code.

We're just working on the web here, but note that your Firebase projects can be used on both iOS and Android apps as well (like, simultaneously).

Let's consider the data for a blog post.

We have to put our content hats on for a minute and consider the bits of data that a blog post might have. Let's keep it real simple and go with title , content , posted date , and author. I'm sure you can imagine a more elaborate data structure, but this'll do for now. You aren't locked to this, it can always be changed later.

Given these, we can structure our first implementation data like this, with the created field being a Unix timestamp:

{ 
  "posts": {
    "101": {
      "title": "Introduction to the Thesis of Theseus",
      "content": "Lorem ipsum dolor sit amet, ...",
      "created": 1483355533000,
      "author": "Duncan"
    },
    "102": {
      ...
    }
  }
}

Let's import some fake data.

This will be easier for us to play around with some data in Firebase we can get our hands on.

  1. Download the posts.json file as raw from here, Or create your own JSON file with the fields you can see in that example.

  2. To initialize the data storage with our posts, select Database from the left side menu under the DEVELOP section.

  3. Click the "Get Started" button.

  4. Open up the ... menu on the right side of the menus, above the dismiss button, for access to the import/export menus

  5. Select Import JSON menu item and the Browse button, selecting the posts.json file downloaded to import.

    If you have an existing project you're reusing, this action will overwrite any data you currently have stored in your project, making a new project recommended for this tutorial.

  6. Select "Import" to save the initial data.

We now have our Firebase project created, development data in our data storage, and are ready to start developing in earnest!

Step 2) Set Up Cloud Functions

First, let's make something clear: You don't need to work with Cloud Functions use Firebase realtime databases. We could make this project entirely through client-side JavaScript. We went the route of Cloud Functions because they are powerful and a big part of understanding the possibilities of **serverless*. Now let's get into what they are.

Firebase Cloud Functions are (nominally) small pieces of code that run on discrete data. In our case, we want to start with a function that will provide a list of all posts.

While the setup is slightly different for Cloud Functions than for many front-end libraries, developers comfortable with JavaScript on the front-end will feel at home with the Firebase Cloud Functions.

  1. On the command line, install the firebase tools globally:
    npm install -g firebase-tools
    
  2. Once installed, create and/or navigate to your project directory. This is where the back-end, serverless code resides locally. We'll develop, test, and deploy from this project directory.

    In the project directory, run:

    firebase login
    
  3. The login command will provide a URL and open your default browser, where you can log in to your google account, and grant access to the firebase command line tools.

    Of note, I find that the authentication process happens entirely in the browser particularly awesome. That the process can be done from other devices if you're on a headless server, say, is even more spectacular. You can, for example, confirm the tools in your phone's browser. Totally awesome.

    $ firebase login
    ? Allow Firebase to collect anonymous CLI usage and error reporting information? No
    Visit this URL on any device to log in:
    https://accounts.google.com/o/oauth2/auth?client_id=123-
    456.apps.googleusercontent.com&scope=...
    Waiting for authentication...
    ✔ Success! Logged in as kitt@example.com
    

    With a successful login, we can develop our project locally and deploy our tested Cloud Functions to Firebase.

  4. In the project directory, run:

    firebase init functions
    

    Firebase has a number of features, functions is only one of them. You can also have hosting, file storage, and event triggers. We'll use only functions initially.

    ? Select a default Firebase project for this directory:
    [don't setup a default project]
    ❯ Duncan (duncan-131)
    Firebase Demo Project (fir-demo-project)
    [create a new project]
    
    === Functions Setup
    
    A `functions` directory will be created in your project with a Node.js
    package pre-configured. Functions can be deployed with firebase deploy.
    
    ? What language would you like to use to write Cloud Functions? JavaScript
    ✔ Wrote functions/package.json
    ✔ Wrote functions/index.js? 
    
    Do you want to install dependencies with npm now? (Y/n)
    

    Allow firebase to install the dependencies by saying yes, to the dependencies install prompt:

    ? Do you want to install dependencies with npm now? Yes
    ...
    ✔ Firebase initialization complete!
    

    We'll now have the following files, and be ready to start developing our back-end API:

    • .firebaserc
    • firebase.json
    • functions/index.js
    • functions/package.json
    • functions/node_modules

Step 3) API development: List All Posts

  1. Open up the functions/index.js file in your favorite editor.

    To make development easier, the Firebase functions module is already included. Our code will use this library to create functions and triggers in firebase.

    We want to access the database, too, so include the firebase-admin module, too.

    // Firebase SDK call to create Cloud Functions and setup triggers.
    const functions = require('firebase-functions');
    
    // The Firebase Admin SDK to access the Firebase Realtime Database. const admin = require('firebase-admin'); 
    // new
    admin.initializeApp(functions.config().firebase); // new 
    
  2. The first end API point we want lists all the posts we have in our blog.

    In the functions/index.js file, add this code:

    // List all the posts under the path /posts/
    exports.posts = functions.https.onRequest((req, res) => {
      return admin.database().ref('/posts').once('value').then(function(snapshot) {
          res.status(200).send(JSON.stringify(snapshot));
      });
    });
    

    We are defining the cloud function posts. In it, we are using the Firebase real-time database (essentially a JSON object) for all the values below the /posts reference, which is at the top (or root) of our JSON.

    Accessing values in the JSON is via the path into the JSON.

    once() is a database action that triggers data retrieval one time. In our code, we want the value event. The value event is sent every time data is changed at or below the reference specified in the ref() call. Because every data change will trigger the value event, use it sparingly.

    When the database reading is done and we have the once() call complete, we can send the snapshot of our data back on the request.

  3. Now that we have code for our first API endpoint, we want to test it. We never, ever want to deploy untested code.

    To test locally before deploying, the firebase command has a serve function that creates a local server which uses the remote datastore - we can access our database locally.

    First run:

    firebase use --add
    

    And select your project you've already started. Then to start the local server, from your project directory, run:

    firebase serve --only functions
    

    This will start the local server and print a URL like:

    http://localhost:5000/PROJECT_NAME/PROJECT_REGION/posts
    

    If the command doesn't open a new browser window, copy the localhost URL into your browser.

    $ firebase serve --only functions
    === Serving from '/Users/codepen/duncan-131'...
    i functions: Preparing to emulate functions.
    ✔ functions: posts: http://localhost:5000/duncan-131/us-central1/posts
    

    If all went well, you'll see the static JSON uploaded earlier.

  4. Next, we want to upload our functions to Firebase. On the command line, in the project directory:

    firebase deploy --only functions
    

    The successful deployment will print out our API URL:

    $ firebase deploy
    === Deploying to 'duncan-131'...
    i deploying functions
    i functions: ensuring necessary APIs are enabled...
    ✔ functions: all necessary APIs are enabled
    i functions: preparing functions directory for uploading...
    i functions: packaged functions (1.04 KB) for uploading
    ✔ functions: functions folder uploaded successfully
    i functions: updating function posts...
    ✔ functions[posts]: Successful update operation.
    Function URL (posts): [https://us-central1-duncan-131.cloudfunctions.net/posts](https://us-central1-duncan-131.cloudfunctions.net/posts) 
    
    ✔ Deploy complete!
    Project Console: https://console.firebase.google.com/project/duncan-131/overview
    

    Viewing this URL, we see our development JSON:

    Yay! We're ready to use our API now.

Step 4) Setting up the Front End

This is where CodePen comes in! We need someone where to build our front end. If you're just playing around, creating a Pen will do. If you're looking to really build this thing out, you'll probably want to create a Project (so ultimately you can break things up into multiple files, upload other resources, deploy it, etc.).

Here's where we're at so far:

See the Pen A Serverless Blog by CodePen (@codepen) on CodePen.

Let's start with some clean HTML

We'll ultimately be asking for posts data from our API, and formatting it into HTML. It's usually a smart idea to start with good HTML first. Here's an example of that, including a div with an id we can easily target with JavaScript to insert our articles ourselves later.

<main class="site-wrap">
  <div id="posts-list">
    <article class="article-block">
      <h2>This is the title of a blog post</h2>
      <time>December 4, 2018</time>
      <div class="excerpt">
        <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Odit harum in ea consequuntur sit neque. Natus accusantium, ducimus, veritatis voluptas animi perspiciatis voluptate similique nihil quam vero dolorem atque error?</p>
      </div>
      <a href="#0">Read Post</a>
    </article>
  </div>
</main>

Next, let's load in the posts via an Ajax call.

var blog_api_url = 'YOUR_FIREBASE_CLOUD_FUNCTIONS_URL';
var posts_list = document.getElementById('posts-list');
var posts_container = posts_list.querySelector('.posts-container');

var loadJsonFromFirebase = function(url, callback) {
 var xhr = new XMLHttpRequest();
  xhr.addEventListener("load", function () {
    callback(JSON.parse(this.response));
  });
  xhr.open("GET", url);
  xhr.send();
};

loadJsonFromFirebase(blog_api_url, function(data) {
  let list = document.createElement('div');
  Object.keys(data).forEach(function(key) {
    let ts = data[key].created;
    list.innerHTML += `<article class="article-block">
      <h2>${data[key].title}</h2> 
      ${ts.toDateString()} 
      <div class="excerpt"> 
        <p>${data[key].content.substr(0, 150)}...</p> 
      </div> 
      <a href="#${key}">Read Post</a>
   </article>`;
  });
  posts_container.insertBefore(list, posts_container.firstChild);
});

Uh oh! CORS problems...

If we refresh the demo right now, you'll see our posts don't load. Viewing the JavaScript console, you can see the problem. We didn't set up our CORS headers:

To add CORS headers, we can include the CORS module. This command, run from the project's functions directory, will grab this module for us and add it to the package.json file:

npm install --save cors

Next, we'll update functions/index.js to use the cors() call. Start by including the cors package at the beginning of the file, then wrap the returned data with the cors() call:

const cors = require('cors')({origin: true});

// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');

// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

// List all the posts under the path /posts/
exports.posts = functions.https.onRequest((req, res) => {
  cors(req, res, () => {
    return admin.database().ref('/posts').once('value').then(function(snapshot) {
      res.status(200).send(JSON.stringify(snapshot));
    });
  });
}

For the changes to take effect, we need to send them to Firebase, which we do with another deploy call. Run this from the project's functions directory:

firebase deploy --only functions

Now when we reload the demo, our data loads! Yay!

The final code for this section is located in this GitHub repo for your reference.

Step 5) Single Posts (and improving our API)

As with most APIs like this, it should allow us to fetch a list of posts, or full individual posts as well.

Good API design would have us create GET URL's like /posts for the entire list (as we already have) and /posts/ID for a single post.

This means we need to add a parameter to our API URL.

The exports.posts syntax we used previously doesn't allow parameters in the URL, so we need to adjust our firebase functions code to use an Express app, which does allow URL parameters in its routes.

We begin this change by installing Express in the functions directory, and saving the dependency to the package.json file at the same time:

npm install express --save

Next, we'll adjust the request handling function by loading Express, then adding the two /posts and /posts/ID routes, as detailed in the Express routing guide :

const cors = require("cors")({ origin: true });

// The Cloud Functions for Firebase SDK to create functions & triggers.
const functions = require("firebase-functions");

// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require("firebase-admin");
admin.initializeApp(functions.config().firebase);

// The express app used for routing
const app = require("express")();

// List all the posts under the path /posts
app.get(
  ["/", "/:id"],
  functions.https.onRequest((req, res) => {
    const postid = req.params.id;
    let reference = "posts";
    reference += postid ? "/" + postid : "";

    cors(req, res, () => {
      return admin
        .database()
        .ref(reference)
        .once("value")
        .then(function(snapshot) {
        if (snapshot.val() !== null) {
          res.status(200).send(JSON.stringify(snapshot));
        } else {
          res.status(200).send({});
        }
      });
    });
  })
);

// set the routes up under the /posts/ endpoint
exports.posts = functions.https.onRequest(app);

As always, test locally:

firebase serve --only functions

When testing, /posts/ should return a JSON list of all of our test posts. The route /posts/101 should return the JSON for the single post with ID 101. The route /posts/aaa should return an empty JSON object, an error state we'll fix shortly.

What we might notice in testing is that while /posts/ returns our full JSON posts list, /posts returns an error Cannot GET null. This is because the router is receiving an empty URL path. This is an odd quirk of the router that we can work around by replacing our onRequest call with a check for the missing trailing slash:

exports.posts = functions.https.onRequest((req, res) =&gt; {
  // Handle routing of /posts without a trailing /,
  if (!req.path) {
    // prepending "/" keeps query params, path params intact
    req.url = `/${req.url}`;
  }
  return app(req, res);
});

Once we are happy with our manual testing, we can deploy our full list and individual posts API:

firebase deploy --only functions

Once we have our individual post API endpoint, we can use it! Let's add the HTML and JavaScript to display a single post.

Add the HTML element that an individual post can load into:

<article id="article" class="article-whole">
</article>

Here's the JavaScript that will grab, format, and display a single post:

...

var getQueryParam = function(param) {
  let params = window.location.search.substr(1);
  params = params.split("&amp;");
  let paramList = {};
  for (let i = 0; i  {
  let post_id = e.target.dataset.post;
  if (post_id) {
    // load the post from ajax call
    loadJsonFromFirebase(blog_api_url + '/' + post_id, function(data) {
      let div = document.createElement('div');
      let ts = data.created;
      div.innerHTML = `
        <h1>${data.title}</h1>
          ${ts.toDateString()}
          <div class="article-body">
            <p>${data.content}</p> 
          </div>
        `;
      post_full.replaceChild(div, post_full.firstChild);
      // hide the full list
      posts_container.classList.add('start-hidden');
      // show the single post
      post_full.classList.remove('start-hidden');
    });
  }
};

// handle the bubbled-up click event
posts_list.addEventListener('click', showPostClick);

...

Now, when we click on the "Read More" links from our posts list, the individual blog entry displays.

We can quickly see where we can add UX enhancements such as adding a spinner when we select "Read More" to indicate to the viewer a post is loading, or a link to return to the list of posts. Creating new posts, however, is more exciting, so let's do that first.

Here's our progress so far:

See the Pen A Serverless Blog by CodePen (@codepen) on CodePen.

Step 6) Creating New Blog Posts

What we've done so far is fetch existing posts and display that. That's great, but if we're really building a blog, we need a way to create new blog posts as well.

Our current process for creating a new post would be to upload a JSON directly through the Firebase interface to add a new post. Possible, but that's no way to live. Let's allow ourselves to create a new blog post right from the site itself!

Keeping in line with API best practices, we want to add a POST action to our /posts API endpoint. The fields we have from our GET endpoint are: author, content, created, and title. The server should handle the created time field, so let's focus on the content and title fields.

To the /posts API endpoint with the POST method, we'll send JSON formatted like this:

{ 
  "title": "The Post Title",
  "content": "This is our example content for this post. It is currently unformatted. Formatting is important, though!"
}

To handle this incoming data, let's use the Express .post routing method, adding the new code just before the exports.posts call:

// create a new post
app.post(
  "/",
  functions.https.onRequest((req, res) => {
    cors(req, res, () => {
      // set the content
      let content = req.body.content ? req.body.content.trim() : null;
      if (content === null) {
        res.status(200).send({ error: "Missing content" });
        return;
      }

      // title can be provided, or extracted from the content
      let title = req.body.title
        ? req.body.title.trim()
        : req.body.content.substr(0, 20) + "...";

      // we want the server to set the time, so use the firebase timestamp
      let postDate = admin.database.ServerValue.TIMESTAMP;

      let postData = { title: title, content: content, created: postDate };

      // create a new ID with empty values
      let postKey = admin
        .database()
        .ref("posts")
        .push().key;

      // set() will overwrite all values in the entry
      // update() will overwrite only the values passed in
      admin
        .database()
        .ref("/posts")
        .child(postKey)
        .set(postData, function() {
          // Read the saved data back out
          return admin
            .database()
            .ref("/posts/" + postKey)
            .once("value")
            .then(function(snapshot) {
              if (snapshot.val() !== null) {
                res.status(200).send(JSON.stringify(snapshot));
              } else {
                res.status(200).send({ error: "Unable to save post" });
              }
            });
        });
    });
  })
);

We are wrapping our call in the cors() function again.

Of note, we use the set() method here for the creation of the post. The set() method will take all the incoming keys and save them and their data, and only them, removing any other JSON key values in the object. The update() method will save the new values of only the keys provided, making update() great for edits, and set() good for creation.

After adding the post function, we again test locally with firebase serve --only functions . To POST to the endpoint, we can use curl:

curl -X POST -H "Content-Type:application/json" -d '{ "title" : "this is the post title", "content" : "this is the development post content"}' http://localhost:5000/PROJECT/REGION/posts

To make testing easier, save the CURL calls into a bash script. The bash script makes initial testing of multiple calls with different JSON values easier, too.

If the CURL POST call works, we will see the additional post item on the posts list when we refresh the pen.

When everything is working, do the ol' firebase deploy --only functions to deploy.

With these changes, we've done several things with our data:

  1. we've ignored the author field
  2. we've just opened up our application to a huge security hole.

The former is part of the development iteration process, which test cases will help find. The last is a huge concern. We don't check or clean the incoming data for hacks, we don't check or clean the data we send back out, and we currently allow anyone to post to the URL, as we don't have any authentication on the POST endpoint.

Let's fix that last one by adding authentication. The first thing we want to do is lock down the API by limiting access. Navigate to the project's Firebase console:

Selecting the Add Firebase to your web app will trigger a popup, with code that will handle authentication in the blog app.

Copy these values, and paste them at the bottom of the HTML in the Pen.

Next, we'll select the sign-in methods allowed for the blog. In the Firebase console, under the Development tab on the left, select the Authentication menu.

Select the big Set Up Sign-In Method button for a list of providers.

In the list, select the providers you'd like to support. Firebase provides a fantastic list of providers.

Select at least one.

Next, add "codepen.io" to the list of authorized domains on the sign-in method page. If you're using Projects, add "codepen.plumbing" and "codepen.website" as well.

Next, let's check for authentication on the incoming POST call in our app.post() call.

Authentication tokens from the client can be sent in the request headers or in the request body, depending on how we send them from the blog front-end. Let's send them in the request body's JSON to reduce complexity, using the token key.

With this, we can use request.body.token value to read the request's authentication token we're going to pass to the server, and Firebase's verifyIdToken() function to confirm the user is valid:

cors(req, res, () => {
  // we have something TO post, confirm we are ALLOWED to post
  const tokenId = req.body.token;
  admin
    .auth()
    .verifyIdToken(tokenId)
    .then(function(decodedUser) {
      // the rest of the app.post function inside the
    })
    .catch(err => res.status(401).send(err));
});

Using the decoded user fields, we can extract the user's name for storing in our post's JSON. This value is good for display, but not unique. As such, we'll use it in this iteration, but refactor to use the unique ID Firebase provides later.

/* 
 * @see [https://firebase.google.com/docs/auth/admin/verify-id-tokens](https://firebase.google.com/docs/auth/admin/verify-id-tokens) 
 * decoded User fields: aud, auth_time, email, email_verified, exp,
 * iat, iss, name, picture, sub, uid, user_id
 */

// For the first pass, use user's Name. This isn't unique.
let postAuthor = decodedUser.name;

Next, let's clean the input data from the user.

Following OWASP's XSS Prevention Cheat Sheet recommendations, we can use the sanitize-html module to strip all HTML from the submitted post data. We can later iterate and add back tags we want to preserve.

As before with the cors module, in our functions directory, we add the sanitize-html module with npm

npm install --save sanitize-html

Then we'll, again, update functions/index.js, but this time to use the sanitizeHtml() call. Start by including the sanitize-html package after our cors initializer:

...
const cors = require('cors')({origin: true});
const sanitizeHtml = require('sanitize-html');
...

Then, clean up our content and title variables using the sanitizeHtml call:

...
let content = req.body.content ? sanitizeHtml(req.body.content, { allowedTags: [], allowedAttributes: [] }) : null;
...
let title = req.body.title ? sanitizeHtml(req.body.title, { allowedTags: [], allowedAttributes: [] }) : content.substr(0, 20) + '...';
...

And our last data change will be to include the newly created post's ID in the returned JSON:

let postJSON = snapshot.val();
postJSON.id = postKey;
res.status(200).send(JSON.stringify(postJSON));

The updated app.post() function in full:

// create a new post
app.post(
  "/",
  functions.https.onRequest((req, res) => {
    cors(req, res, () => {
      let content = req.body.content ? sanitizeHtml(req.body.content, { allowedTags: [], allowedAttributes: [] }) : null;
      if (content === null) {
        res.status(200).send({ error: "Missing content" });
        return;
      }
      // we have something TO post, confirm we are ALLOWED to post
      const tokenId = req.body.token;
      admin
        .auth()
        .verifyIdToken(tokenId)
        .then(function(decodedUser) {
          // title can be provided, or extracted from the content
          let title = req.body.title ? sanitizeHtml(req.body.title, { allowedTags: [], allowedAttributes: [] }) : content.substr(0, 20) + '...';
          // we want the server to set the time, so use firebase timestamp
          let postDate = admin.database.ServerValue.TIMESTAMP;
          /*
     * @see [https://firebase.google.com/docs/auth/admin/verify-id-tokens](https://firebase.google.com/docs/auth/admin/verify-id-tokens) 
     * decoded User fields: aud, auth_time, email, email_verified, exp, iat, iss, name, picture, sub, uid, user_id */
          // For the first pass, use user's Name. This isn't unique
          let postAuthor = decodedUser.name;

          // assembled data
          let postData = {
            author: postAuthor,
            title: title,
            content: content,
            created: postDate
          };

          // create a new ID with empty values
          let postKey = admin
            .database()
            .ref("posts")
            .push().key;

          // set() will overwrite all values in the entry
          admin
            .database()
            .ref("/posts")
            .child(postKey)
            .set(postData, function() {
              // Read the saved data back out
              return admin
                .database()
                .ref("/posts/" + postKey)
                .once("value")
                .then(function(snapshot) {
                  if (snapshot.val() !== null) {
                    let postJSON = snapshot.val();
                    postJSON.id = postKey;
                    res.status(200).send(JSON.stringify(postJSON));
                  } else {
                    res.status(200).send({ error: "Unable to save post" });
                  }
                });
            });
        })
        .catch(err => res.status(401).send(err));
    });
  })
);

Now when we test locally with our updated post function, again with firebase serve --only functions, we have authentication failures.

If we test on a second server, we also have CORS errors again. We can add the CORS middleware in the Express routing with this code outside of the app.get() and app.post() calls:

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Accept"
  );
  next();
});

Test and deploy: firebase serve --only functions followed by firebase deploy --only functions.

The last bit we need to add is the ability to submit blog posts from our front-end. We previously added the Firebase authentication code to our HTML. Let's add a "Sign In" button to trigger the Firebase authentication process, and a "New Post" button, which we can display after sign-in:

<a id="sign-in-button" class="button sign-in-button">
  <svg width="24" height="24" viewBox="0 0 24 24">
    <path d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"></path>
  </svg>
  Sign In
</a>
<a id="new-post-button" class="button sign-in-button start-hidden">
  <svg width="24" height="24" viewBox="0 0 24 24">
    <path d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z"></path>
  </svg>
  New Post
</a>

Then, add the JavaScript to enable the authentication, which will trigger toggling the buttons' displays:

// track authenticated user to avoid triggering on refresh
var currentUID;

// Bindings on load.
document.addEventListener("DOMContentLoaded", function() {
  document
    .getElementById("sign-in-button")
    .addEventListener("click", function() {
      var provider = new firebase.auth.GoogleAuthProvider();
      firebase.auth().signInWithPopup(provider);
    });

  // Listen for auth state changes
  firebase.auth().onAuthStateChanged(onLogInOutChange);
});

// toggle buttons on sign in/out auth changes
var onLogInOutChange = function(user) {
  // Ignore token refresh events
  if (user && currentUID === user.uid) {
    return;
  }

  // If logged in, show the new post button
  if (user) {
    currentUID = user.uid;
    document.getElementById("sign-in-button").style.display = "none";
    document.getElementById("new-post-button").style.display = "block";
  } else {
    currentUID = null;
    document.getElementById("sign-in-button").style.display = "block";
    document.getElementById("new-post-button").style.display = "none";
  }
};

After adding the JavaScript, give the log in a try. The "New Post" button should display after authentication, and only if a user is authenticated.

We can't rely on the button's display state for authentication status - a simple HTML change in the pen can display the "New Post" button. However, because we check authentication status in the POST API method, we are okay if this HTML change happens, as it won't introduce any security issues.

Now that we have our "New Post" button displaying after authentication, add the new post form:

<section id="new-post" class="article-form" style="display: none;">
  <h2>New Post</h2>
  <form id="message-form" action="#">
    <h4><label for="new-post-title">Title</label></h4>
    <input type="text" id="new-post-title">
    <h4><label for="new-post-content">Content</label></h4>
    <textarea rows="3" id="new-post-content"></textarea>
    <button type="submit">Add post</button>
  </form>
</section>

And trigger its display on the "New Post" button click:

// show the new post form
document.getElementById('new-post-button').addEventListener('click', function() {
  document.getElementById('new-post').style.display = '';
});

Next, connect the message submit button to the form submit to our API endpoint. We'll want to include the authentication token that Firebase provides for the submission.

// Saves message on form submit.
let messageForm = document.getElementById("message-form");
messageForm.onsubmit = function(e) {
  e.preventDefault();

  let postTitle = document.getElementById("new-post-title");
  let postContent = document.getElementById("new-post-content");
  let title = postTitle.value;
  let content = postContent.value;

  if (content) {
    // if the user is logged in, continue
    firebase
      .auth()
      .currentUser.getIdToken(/* forceRefresh */ true)
      .then(function(idToken) {
        var xhr = new XMLHttpRequest();
        xhr.addEventListener("load", function() {
          // on success, display post @ top of the list and hide the form
          postTitle.value = "";
          postContent.value = "";
          document.getElementById("new-post").style.display = "none";

          let postDetails = JSON.parse(this.response);
          let div = document.createElement("div");
          let ts = postDetails.created;
          div.innerHTML += `<article class="article-block">
             <h2>${postDetails.title}</h2>
             ${ts.toDateString()}
             <div class="excerpt">
               <p>${postDetails.content.substr(0, 150)}...</p>
             </div>
             <a>Read Post</a>
           </article>`;
          posts_container.insertBefore(div, posts_container.firstChild);
        });

        xhr.open("POST", blog_api_url);
        xhr.setRequestHeader("Content-Type", "application/json");
        xhr.send(
          JSON.stringify({ title: title, content: content, token: idToken })
        );
      })
      .catch(function(error) {
        // otherwise, TODO: handle error, user isn't authenticated yet
      });
  } else {
    // TODO: display box around the missing content field
  }
};

We're here!

See the Pen A Serverless Blog by CodePen (@codepen) on CodePen.

Step 7) Handle URL Changes (and cleaning up)

We now have a list of all of our posts, we can view single posts, and we can create new posts. The site has the authentication "feature" that anyone with a Google account can create a new post, as well as the limitation that we are unable to link to a specific post.

We also know we can make the post rendering easier by creating a template, so let's start there, before tackling URL handling and authentication limits.

First up, create a render function that can render both a full post, as well as an excerpt:

const renderPost = function(postId, postData, summary = false) {
  let ts = new Date(postData.created);
  if (summary) {
    return `<article class="article-block">
      <h2>${postData.title}</h2>
      ${ts.toDateString()}
      <div class="excerpt">
        <p>${postData.content.substr(0, 150)}...</p>
      </div>
      <a>Read Post</a>
    </article>`;
  } else {
    return `<article class="article-block">
      <h2>${postData.title}</h2>
      ${ts.toDateString()}
      <div class="excerpt">
        <p>${postData.content}</p>
      </div>
    </article>`;
  }
}

Then, change the rendering calls to use the new function:

// example, in messageForm.onsubmit function
...
  let postDetails = JSON.parse(this.response);
  let div = document.createElement('div');
  // this is the replaced rendering section
  div.innerHTML += renderPost(postDetails.id, postDetails, false);
  posts_container.insertBefore(div, posts_container.firstChild);
...

// example, in showPostClick
...
  let div = document.createElement('div');
  // this is the replaced rendering function
  div.innerHTML = renderPost(post_id, data, false);
  post_full.replaceChild(div, post_full.firstChild);
...

// example in initial page data load
...
  let list = document.createElement('div');
    Object.keys(data).forEach(function(key) {
      // this is the new replaced code
      list.innerHTML += renderPost(key, data[key], true);
    });
  posts_container.insertBefore(list, posts_container.firstChild);
...

Next, let's handle the URL change states - we want the browser URL to reflect the blog's content, either a specific post or the list of posts, that we load via the Firebase AJAX call. This is the perfect use for the HTML5 History API, updating the URL on content changes, and using the updated URL to load individual posts.

First, a function to extract the post_id from the URL:

const getAnchorParam = function() {
  return (window.location.href.split('#').length > 1) ? window.location.href.split('#')[1] : null;
}

Then, using it when the page loads, replacing the previous page load code with this new function:

// load everything up
let post_id = getAnchorParam();
if (post_id) {
  loadJsonFromFirebase(blog_api_url + post_id, function(postData) {
    let list = document.createElement('div');
    list.innerHTML += renderPost(postData.id, postData, true);
    posts_container.insertBefore(list, posts_container.firstChild);
  });
} else {
  loadJsonFromFirebase(blog_api_url, function(data) {
    let list = document.createElement('div');
    Object.keys(data).forEach(function(key) {
      list.innerHTML += renderPost(key, data[key], true);
    });
    posts_container.insertBefore(list, posts_container.firstChild);
  });
}

And last, we need to add our URL pushState() and popstate() handlers:

At the end of our showPostClick function, add the code to save the post_id to the History API queue:

  const showPostClick = (e) => {
    let post_id = e.target.dataset.post;

    if (post_id) { 
      ...
      // update the URL if we are not traversing history state
      if (!e.skipPushState) {
        history.pushState( { post_id: post_id }, null, '#/' + post_id);
      }
    }
  }

And again at the end of the showListClick function:

const showListClick = (e) => {
  // hide the single post
  post_full.classList.add('start-hidden');
  // show the full list
  posts_container.classList.remove('start-hidden');

  // adjust the URL back to the full list
  if (!e.skipPushState) {
    history.pushState({ post_id: 'full-list' }, null, window.location.href.split('#')[0]);
  }

Then, we can handle the browser button state by adding a window popstate listener:


window.addEventListener('popstate', function(e) { let post_id = e.state ? (e.state.post_id ? e.state.post_id : null) : null; // we are using render function that adjust the pushstate, skip when calling here. e.skipPushState = true; // when the post_id is null, we don't have any managed history, so do nothing if (post_id == null) { return; } else if (post_id == 'full-list') { showListClick(e); } else { // add our missing data-post value so that we can use the showPostClick function e.target.dataset = e.target.dataset ? e.target.dataset : {}; e.target.dataset.post = post_id; showPostClick(e); } });

In a Pen here on CodePen, the preview displays in an iframe, which hides the URL. If we switch to a CodePen Project we can deploy it to see the URL changes with the pushState and popState events.

We're here!

See the Pen A Serverless Blog by CodePen (@codepen) on CodePen.

Lastly, we want to limit who can write data to our blog. Currently, the demo permissions are open: anyone authenticated with a Google account can read and write posts. We can change these access rules in the Firebase Console.

Navigate to your Firebase console

Select the Database Tab under the Develop menu on the left side, then select the Rules tab in the main window.

Show Database Rules location in Firebase Console

We can see the default rules are "Site visitors are authenticated." We can update our rules so that anyone can access the API by changing the .read value:

{
  "rules": {
    ".read": true,
    ".write": "auth != null"
  }
}

If we have different API endpoints, more than just our posts endpoint, we could limit who can read the endpoints, by specifying the endpoints in our rules:

{
  "rules": {
    "posts": {
      ".read": true,
      ".write": "auth != null"
    },
    "admin": {
      ".read": "auth.uid === 'abcdef'",
      ".write": "auth.uid === 'abcdef'"
    }
  }
}

An easy example for a single user blog is to limit the write access to a single user:

{
  "rules": {
    "posts": {
      ".read": true,
      ".write": "auth.uid === 'abcdef'"
    }
  }
}

A user's UID (abcdef is an invalid UID) can be found in the Authentication section of the Develop menu, in the Users tab in the main window:

Copy User uid from the Firebase Console Develop Authentication pane

This limits who can create a post to only a single authenticated account, which wraps up this serverless tutorial. We can view all our posts, view a single post, submit a new post, use the browser history, and authenticate users, all without spinning up a single server.

Keep on going!

While we now have the basics of a blog hosted on CodePen using Firebase, all serverless, in a production scenario, we would set up development, staging, and production versions, then keep iterating: improving the design and adding features. Here are some ideas:

  • Loading icons indicating API calls are happening
  • A Markdown editor for styling
  • Comments on posts from authenticated guest users
  • Voting or favoriting posts
  • Editing of posts by implementing the PUT action in our API
  • Communal blogs with multiple authors, including filtering by author
  • Refactor the author field with unique IDs provided by Firebase authentication, along with their displayed name
  • Pagination
  • Use something like Angular, Vue, or React

Again, we do all of this without having to spin up a traditional server. Awesome!