Intro to Serverless

Upkeep of a server is a lot of work. It's easier to run server-side code on someone else's managed server that doesn't require your maintenance. These are Netlify's serverless functions. We're going to build an extra-simple bookmark app that uses serverless functions and Fauna DB, a new database structure built to accommodate serverless.

Setting Up Fauna and GraphQL

I'm registering an account with Fauna, then importing a GraphQL Schema into a new database. GraphQL is an aggregated way of handling REST API requests. That means it allows you to get different data from different places without having to change anything-- it's a consolidated place to request everything you need.

I import the following GraphQL scheme into Fauna:

type Link {
  url: String!
  name: String!
  description: String!
  archived: Boolean
}

type Query {
  allLinks: [Link!]!
}

The exclamation points indicate these fields are required in GraphQL. There's a GraphQL tab in the Fauna dashboard where you can import the schema, then play around with it.

We can write queries in the playground-- Ctrl + Space will bring up Intellisense.

Here's an example query in the playground:

`
 query {
    allLinks {
      data {
        name,
        url,
        description,
        _id,
        archived
      }
    }
  }
`

You can also do mutations here. Mutations are like functions in that you pass them parameters-- we'll be creating functions for creating links, updating links, and deleting links.

`mutation{
  createLink (data: {name: "Joe's site", url: "https://joeholmes.me", description: "My website", archived: false}) {
    name,
  }
}`

This creates a link that can be queried with the code above.

Netlify CLI Functions Intro

Using Netlify's command line interface, we can run serverless functions locally and test them. `npm install netlify-cli`.

I created a new react app and added a folder to it called 'functions'. Serverless functions export a handler, so that's how it starts:

//functions/helloWorld.js

exports.handler = async (events, context, callback) => {
  return {
    statusCode: 200,
    body: JSON.stringify({ msg: "Hello world" }),
  };
};

We also need to configure how Netlify runs this locally. I created a file called `netlify.toml` in root and added to it:

[build]
    functions = 'functions'

Now run `netlify dev` and at http://localhost:8888/.netlify/functions/helloWorld it should say "hello world".

Env Variables in Fauna

In the security tab in Fauna, create a new key. Make it a "server" key, then insert it in a .env file as `FAUNA_SECRET_KEY`. Don't forget to add the file to your `.gitignore`.

GET Function to Retrieve Info from Fauna

I created a new function in the functions folder called `getLinks`. Then `npm i axios` and `npm i dotenv` as dependencies. We'll send the GraphQL query via an HTTP REST request.

const axios = require("axios");
require("dotenv").config();

exports.handler = async (event) => {
  const GET_LINKS = `
query{
    allLinks {
        data {
            name
            _id
            url
            description
            archived
        }
    }
}            
`;
//destructures data in response
  const { data } = await axios({
    url: "https://graphql.fauna.com/graphql",
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.FAUNA_SECRET_KEY}`,
    },
    data: {
      query: GET_LINKS,
  //variables only used in mutations
      variables: {},
    },
  });
  console.log(data);
  return {
    statusCode: 200,
    body: JSON.stringify(data),
  };
};

Pretty sweet-- now going to `/.netlify/functions/getLinks` returns the stringified JSON data of our link post.

We then refactored the utilities in the above snippet, so that `formattedResponse` with a status code and body was in its own file, as well as the content of the query.

Create/Update/Delete Link Methods

const CREATE_LINK = `
    mutation($name: String!, $url: String!, $description: String! ) {
        createLink( data: { name:$name, url: $url, description: $description, archived: false }) {
            name
            _id
            url
            description
            archived
        }
    }
`;

Since it's a mutation, we pass in variables. These will eventually be the values in a createLink form.

Had to set up Postman to test this API, which was tricky and time-consuming at first. Had to go to "body" and "raw" and input the createLink info manually, then clicked test at `localhost:8888/.netlify/functions/createLink`. After much work, it went through.

Updated links and deleted links are more or less the same-- we change the requests to "PUT" and "DELETE" instead of "POST", and in the case of DELETE took all info from the link away except for the id.

For instance, here's the refactored update function:

  
const axios = require("axios");
const formattedResponse = require("./utils/formattedResponse");
require("dotenv").config();
const { UPDATE_LINK } = require("./utils/linkQueries.js");
const sendQuery = require("./utils/sendQuery");

exports.handler = async (event) => {
  if (event.httpMethod != "PUT") {
    return formattedResponse(405, { err: "Method not supported" });
  }
  const { name, url, description, _id: id, archived } = JSON.parse(event.body);
  const variables = { name, url, description, id, archived };
  try {
    const { updateLink: updatedLink } = await sendQuery(UPDATE_LINK, variables);
    return formattedResponse(200, updatedLink);
  } catch (err) {
    console.log(err);
    return formattedResponse(500, { err: "something wrong" });
  }
};

Frontend

We installed Bootstrap to make it easy to get something up.

We used React hooks to structure things-- `useEffect` lets us decide when we want to load the links and `useState` tracks the links status over time.

We're using the `fetch` API since it is built into JS for the frontend-- I was unsure here, but I think this means on the backend we need Axios/other stuff.

In each async function, James wraps them in try/catch sequences, and says this is always best practice. For example:

  const archiveLink = async () => {
    link.archived = true;
    try {
      await fetch("/api/updateLink", {
        method: "PUT",
        body: JSON.stringify(link),
      });
    } catch (error) {
      console.error(error);
    }
  };

  const deleteLink = async () => {
    const id = link._id;
    try {
      await fetch("/api/deleteLink", {
        method: "DELETE",
        body: JSON.stringify({ id }),
      });
    } catch (error) {
      console.error(error);
    }
  };

Note the second parameter in the fetch calls, for the methods of the REST API. I believe the default request is GET.

After setting up archive and delete links, we set up a function that runs after the `await` call that refreshes the page. He recommends using the Context API for more serious projects.

Form Handling

This we breezed through, and it reminded me I needed a refresher on form syntax. In the below snippet, `useState` is called to store field information, a reset function is described that returns them to empty values, and the refreshLinks call mentioned above is used again.

//function that handles the create links form

export default function LinkForm({ refreshLinks }) {
  const [name, setName] = useState("");
  const [url, setUrl] = useState("");
  const [description, setDescription] = useState("");

  const resetForm = () => {
    setName("");
    setDescription("");
    setUrl("");
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    const body = { name, url, description };
    try {
      const res = await fetch("/.netlify/functions/createLink", {
        method: "POST",
        body: JSON.stringify(body),
      });
      resetForm();
      refreshLinks();
    } catch (error) {
      console.error(error);
    }
  };

We used Bootstrap for the forms, so it's not relevant for me at the moment. But the important parts of the html are:

  • In the `<input>` tags, the value is passed as the JS variable defined in useState (ie, `value={url}`)
  • An `onChange` prop is used that takes in the event and uses the setState function: `onChange={(e)=>setDescription(e.target.value)}`
  • The `<form>` parent tag calls the handleSubmit function mentioned above as `<form onSubmit={handleSubmit}>`.

Deploy Time

Getting it into Netlify was a bit of a pain. Had to reset the netlify.toml file to default, and make sure all warnings were removed since Netlify read them as errors. But it works! I'd post the link here, but that seems unsafe since there are no safeguards from abusing the form. So, a screenshot:

List o Links - Serverless Project