In summary: This was a tough course that had several things way over my head, and I burned a ton of brain-calories over the five hours it took me to work through the (45 minute!) tutorial. I carefully commented all the code, trying to understand what happened at each step. I'll probably take a step back here and wrap my head around some simpler full-stack frameworks (MERN, maybe), but I got a lot out of this.

We end up building a code snippet saver with full CRUD functions (that stands for Create, Read, Update, and Delete). When he comes out with part two and an authentication guide, I will have the tools I need to share something like it with the world.

Source Code

I forgot to checkout the part-one-starter branch and it caused me big headaches.

Ran git clone https://github.com/jamesqquick/nextjs-crash-course-fauna-swr-react-hook-form-tailwind.git next-fauna-cc`. THEN, git checkout part-one-starter or else it's all a mess.

He put all the goodies in there already, which I'm appreciating more and more-- npm install and Tailwind was there, etc.

Go into the FaunaDB dashboard and retrieve a "server" key. Then add the .env variable.

In FaunaDB there's no unique URL to our database, just a key we use with our API.

Using the methods contained in the npm faunadb package:

//utils/Fauna.js

const getSnippets = async () => {
  //By default, querying documents returns ref info; we want content
  //Do that with paginate-- means we go over each ref
  //map iterates through them. it takes two parameters:
  //one, what it iterates through; two, what to do on each
  //we want to do a Lambda function, so thats our second param
  const { data } = await faunaClient.query(
    q.Map(
      q.Paginate(q.Documents(q.Collection("code-snips-cc"))),
      //Lambda takes two params. We call each item in the map 'ref',
      //then we want to get the data associated with that reference
      q.Lambda("ref", q.Get(q.Var("ref")))
    )
  );
  const snippets = data.map((snippet) => {
    //get the id from the ref, because directly
    //getting the id from JSON can be wonky
    snippet.id = snippet.ref.id;
    delete snippet.ref;
    return snippet;
  });
  return snippets;
};

Connecting Fauna Client to Next's API Folder

// in pages/api/getSnippets

import { getSnippets } from '../../utils/Fauna';
export default async function handler(req, res) {
    //if statement requires the request to be a
    //GET request
    if (req.method !== 'GET') {
        return res.status(405);
    }

    try {
        const snippets = await getSnippets()
        //snippets are sent to us from utils/Fauna
        //if this resolves correctly, send allclear
        //status and turn the snips into JSON
        return res.status(200).json(snippets)
    } catch (err) {
        console.error(err);
        res.status(500).json({ msg: 'Something went wrong.' });
    }
}

Using SWR For Data Fetching

SWR seems really cool. It stands for Stale-While-Revalidate, and is a library developed by the makers of Next. It uses React Hooks to fetch for us and takes the lame work out of fetching, as well as making data changes crazy fast.

He moved quickly through mutations using SWR and that was way over my head, so I'm going to worry about it later.

SWR was this simple: const { data: snippets, mutate } = useSWR("/api/snippets");

Creating Snippets

//utils/Fauna.js
const createSnippet = async (code, language, description, name) => {
    //this performs a query in Fauna that creates a record
  return await faunaClient.query(q.Create(q.Collection('snippets'), {
      //tells it what fields to create in the record
      data: {code, language, description, name}
  }))
};

Now that it's defined, we tell the API about it.

//api/createSnippet.js
import { createSnippet } from "../../utils/Fauna";
export default async function handler(req, res) {
  const { code, language, description, name } = req.body;
  if (req.method !== "POST") {
    return res.status(405).json({ msg: "Method not allowed" });
  }
  try {
    //we've already set up Fauna to accept
    //the field values of the new record
    //so now we pass them in as parameters
    //make sure they're in the same order
    const createdSnippet = await createSnippet(
      code,
      language,
      description,
      name
    );
    return res.status(200).json(createdSnippet);
  } catch (err) {
    console.error(err);
    res.status(500).json({ msg: "Something went wrong." });
  }
}

Configuring React Hooks Form

We're using RHF because doing it yourself is tedious.

It returns an object that has a couple different properties. After importing useForm, `const {register, handleSubmit, errors} = useForm()` to pull the props out.

Wrap onSubmit props with RHF's submit handler, handleSubmit().

  //in SnippetForm.js component
const createSnippet = async (data) => {
    const { code, language, description, name } = data;
    try {
      await fetch("/api/createSnippet", {
        method: "POST",
        body: JSON.stringify({ code, language, description, name }),
        headers: {
          "Content-Type": "application/json",
        },
      });
      //takes us back to homepage
      router.push("/");
    } catch (err) {
      console.error(err);
    }
  };

Updating Snippets

Very similar to Create in the utils, but you need to grab the id of the existing item. You use a `q.Ref` method.

const updateSnippet = async (id, code, language, name, description) => {
  //TODO: update snippet
  return await faunaClient.query(
    q.Update(q.Ref(q.Collection("snippets"), id), {
      data: { code, language, name, description },
    })
  );
};

Back to the API!

import { updateSnippet } from "../../utils/Fauna";
export default async function handler(req, res) {
  if (req.method !== "PUT") {
    return res.status(405).json({ msg: "Method not allowed" });
  }
  const { id, code, language, description, name } = req.body;

  try {
    const updatedSnippet = await updateSnippet(
      //note we add the id
      id,
      code,
      language,
      name,
      description
    );
    return res.status(200).json(updatedSnippet);
  } catch (err) {
    console.error(err);
    res.status(500).json({ msg: "Something went wrong." });
  }
}

Now in snippet.js, we have a LINK that sends us to the edit page. Using the usual Next format, the folder "edit" routes all files within it to urls with `edit/[whatever]`. `<Link href={`/edit/${snippet.id}`}>` is ours.

Within edit/[id], we use getServerSideProps()--my first time!

We must prepare by creating a utility that returns the whole snippet if given an ID (so the edit/id screen has the snippet info). In utils/Fauna:

const getSnippetById = async (id) => {
  const snippet = await faunaClient.query(
    //get reference in collection "snippets" that
    //corresponds to this id
    q.Get(q.Ref(q.Collection("snippets"), id))
  );
  //same manuever as above, swaps out ref for id
  snippet.id = snippet.ref.id;
  delete snippet.ref;
  return snippet;
};

Now, for ServerSideProps:


export async function getServerSideProps(context) {
    try {
        //it finds "id" because it's the name in brackets for the file
        const id = context.params.id;
        //now we need to call getSnippetById
        //we pass it the id from the params above
        const snippet = await getSnippetById(id)
        return {
            //this part is native to next,
            //it goes to the top of the function
            //and sends it to what we defined
            props: {snippet},
        };
    } catch (error) {
        console.error(error);
        context.res.statusCode = 302;
        context.res.setHeader('Location', `/`);
        return { props: {} };
    }
}

In the snippet form, we now pass a param into the useForm hook that gives us default values (ie, pulls the existing snippet data into our edit screen).

const { register, handleSubmit, errors } = useForm({
    defaultValues: {
      code: snippet ? snippet.data.code : "",
      language: snippet ? snippet.data.language : "",
      description: snippet ? snippet.data.description : "",
      name: snippet ? snippet.data.name : "",
    },
  });

//then adding updateSnippet to the form component
  const updateSnippet = async (data) => {
    const { code, language, description, name } = data;
    const id = snippet.id;
    try {
      await fetch("/api/updateSnippet", {
        method: "PUT",
        body: JSON.stringify({code, language, description, name, id}),
        headers: {
          "Content-Type": "application/json",
        },
      });
      router.push("/");
    } catch (err) {
      console.error(err);
    }
  };

Deleting snippets was very similar to these two, so I'm skipping it. (I'm also wore out from all that code-parsing).

But I now have a workable framework to build actual software in the internet-- once I learn user authentication I can start putting projects like these out into the world, which is a huge step forward. Now I will go back to some fundamentals.