Chapter 8: Reddit API

Full Stack React Native

link Reddit API

By the end of this lesson, developers will be able to:

  • Structure a GraphQL websocket server
  • Create a GraphQL API with subscriptions

Express API Structure (Full Crud)

We are going to build a posts API with Full CRUD. This means the API will be able to Create, Read, Update and Destroy posts.

Unlike previous servers we will no longer use graphql-yoga. This is because we would like to create a Subscription service using apollo-server-express.

link Subscriptions

Subscriptions are GraphQL operations that watch events emitted from Apollo Server. The native Apollo Server supports GraphQL subscriptions without additional configuration.

Let's view an example application with the ability to subscribe to new comments:

Builder Book

Source

In the above example the GraphQL server "publishes" events so that clients can recieve instants updates. Each time a client submits a new comment, new comments are displayed.

Let's get started!

Let's begin building our Apollo server by creating a new directory called reddit-api:

mkdir reddit-api
cd reddit-api
npm init -y

Then, switch into your new directory and install the following dependencies:

cd reddit-api
yarn add apollo-server-express body-parser cors express graphql graphql-postgres-subscriptions graphql-tools pg sequelize subscriptions-transport-ws && yarn add -D nodemon sequelize-cli

Next we will initialize a Sequelize project:

npx sequelize-cli init

Let's setup our database configuration:

reddit-api/config/config.json

{
  "development": {
    "database": "reddit_api_development",
    "dialect": "postgres"
  },
  "test": {
    "database": "reddit_api_test",
    "dialect": "postgres"
  },
  "production": {
    "use_env_variable": "DATABASE_URL",
    "dialect": "postgres",
    "dialectOptions": {
      "ssl": true
    }
  }
}

Notice: For production we use use_env_variable and DATABASE_URL. We are going to deploy this app to Heroku. Heroku is smart enough to replace DATABASE_URL with the production database. You will see this at the end of the lesson.

Cool, we should now be all setup to create the database. Let's do that:

npx sequelize-cli db:create

Next we will create a Post model:

npx sequelize-cli model:generate --name Posts --attributes title:string,link:string,imageUrl:string

Checkout the Sequelize Data Types that are available: https://sequelize.org/master/manual/data-types.html

Now we need to execute our migration which will create the posts table in our Postgres database along with the columns:

npx sequelize-cli db:migrate

If you made a mistake, you can always rollback: npx sequelize-cli db:migrate:undo

Now let's create a seed file:

npx sequelize-cli seed:generate --name posts
Let's edit the code for the posts seed file
'use strict';
const faker = require('faker');

const posts = [...Array(100)].map((post) => (
  {
    title: faker.company.catchPhrase(),
    link: faker.internet.url(),
    imageUrl: "https://picsum.photos/200/200",
    createdAt: new Date(),
    updatedAt: new Date()
  }
))

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('Posts', posts);
  },

  down: (queryInterface, Sequelize) => {
    /*
      Add reverting commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.bulkDelete('People', null, {});
    */
  }
};

Seed Database

Execute the seed file:

npx sequelize-cli db:seed:all

Made a mistake? You can always undo: npx sequelize-cli db:seed:undo

Drop into psql and query the database for the demo post:

psql reddit_api_development
SELECT * FROM "Posts";

Create a .gitignore file:

touch .gitignore

Insert the following inside .gitignore:

/node_modules
.DS_Store
.env

link Websocket Schema

We will utilize a PostgresPubSub to receive Websocket connections.

Subscriptions depend on use of a publish and subscribe primitive to generate the events that notify a subscription. PostgresPubSub is a factory that creates event generators.

Source

Each time the server boots up, we connect to the websocket server and send live updates.

  • Note the new Subscription type, which defines logic to listen for three websocket events: postAdded, postEdited, and postDeleted.

  • Each of these events are declared as asyncIterator. Async iterators allow us to iterate over data that arrives asynchronously, on-demand.

  • Note that these resolvers publish events with the PostgresPubSub client.

Let's create our websocket client boilerplate:

cd src
touch pubSub.js

Inside the new pubSub.js, add the following snippet:

const { PostgresPubSub } = require('graphql-postgres-subscriptions');
const { Client } = require('pg');

const DATABASE_URL = process.env.DATABASE_URL || 'reddit_api_development';

const client = new Client({
  connectionString: DATABASE_URL,
  ssl: process.env.DATABASE_URL ? true : false
});

client.connect();

const pubSub = new PostgresPubSub({ client });

module.exports = { pubSub };

link Resolvers

Great, let's create a new resolvers directory for the server resolvers map.

cd ..
mkdir resolvers
cd resolvers
touch index.js
touch Mutation.js
touch Query.js
touch Subscription.js

Add the following inside Mutation.js:

const { Posts } = require('../models');
const { pubSub } = require('../src/pubSub');

const addPost = async (_, { title, link, imageUrl }) => {
  const post = await Posts.create({ title, link, imageUrl });
  pubSub.publish('postAdded', {
    postAdded: { id: post.id, title, link, imageUrl }
  });
  return post.id;
};

const editPost = async (_, { id, title, link, imageUrl }) => {
  const [updated] = await Posts.update(
    { title, link, imageUrl },
    {
      where: { id: id }
    }
  );
  if (updated) {
    const updatedPost = await Posts.findOne({ where: { id: id } });
    pubSub.publish('postEdited', { postEdited: updatedPost });
    return updatedPost;
  }
  return new Error('Post not updated');
};

const deletePost = async (_, { id }) => {
  const deleted = await Posts.destroy({
    where: { id: id }
  });
  if (deleted) {
    pubSub.publish('postDeleted', { postDeleted: id });
    return id;
  }
  return new Error('Post not deleted');
};

module.exports = {
  addPost,
  editPost,
  deletePost
};

Add the following inside Query.js:

const { Posts, sequelize } = require('../models');

const posts = async () => {
  const posts = await Posts.findAll({ order: [['id', 'DESC']] });
  return posts;
};

module.exports = { posts };

Add the following inside Subscription.js:

const { pubSub } = require('../src/pubSub');

const postAdded = {
  subscribe: () => pubSub.asyncIterator('postAdded')
};
const postEdited = {
  subscribe: () => pubSub.asyncIterator('postEdited')
};
const postDeleted = {
  subscribe: () => pubSub.asyncIterator('postDeleted')
};

module.exports = { postAdded, postEdited, postDeleted };

Each resolver is now responsible for it's respective queries, mutations, or subscriptions.

Let's tie them together inside index.js:

const Query = require('./Query');
const Mutation = require('./Mutation');
const Subscription = require('./Subscription');

module.exports = {
  Query,
  Mutation,
  Subscription
};

link Apollo Server

Now that the resolvers are ready, we can modify the server boilerplate:

const express = require('express');
const { ApolloServer, gql } = require('apollo-server-express');
const { execute, subscribe } = require('graphql');
const { createServer } = require('http');
const { makeExecutableSchema } = require('graphql-tools');
const { SubscriptionServer } = require('subscriptions-transport-ws');
const cors = require('cors');
const bodyParser = require('body-parser');
const resolvers = require('../resolvers');
const { pubSub } = require('./pubSub');
const port = process.env.PORT || 4000;

const typeDefs = gql`
  type Post {
    title: String!
    link: String!
    imageUrl: String
    id: ID!
  }
  type Query {
    posts: [Post]
  }
  type Mutation {
    addPost(title: String!, link: String!, imageUrl: String!): ID
    editPost(id: ID!, title: String!, link: String!, imageUrl: String!): Post
    deletePost(id: ID!): ID
  }
  type Subscription {
    postAdded: Post
    postEdited: Post
    postDeleted: ID
  }
`;

const schema = makeExecutableSchema({
  typeDefs,
  resolvers
});

const apolloServer = new ApolloServer({
  schema,
  context: request => {
    return {
      ...request,
      pubSub
    };
  },
  introspection: true,
  playground: {
    endpoint: '/graphql'
  }
});

const app = express();
const server = createServer(app);
apolloServer.applyMiddleware({ app });
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cors());

server.listen({ port }, () => {
  console.log(`Server is running at http://localhost:${port}`);
  new SubscriptionServer(
    {
      schema,
      execute,
      subscribe,
      keepAlive: 10000
    },
    {
      server: server
    }
  );
});

link Context

The function for Queries and Mutations contains the arguments for the integration, in Express's case req and res. This is especially important, since the auth tokens are handled differently depending on the transport.

link Test Server

Modify your package.json file to support nodemon. Also, let's create a command npm db:reset that will drop the database, create the database, run the migrations, and seed!

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "nodemon src/index.js",
  "db:reset": "npx sequelize-cli db:drop && npx sequelize-cli db:create && npx sequelize-cli db:migrate && npx sequelize-cli db:seed:all"
},

Let's make sure our server works:

yarn start

Test Query

Open http://localhost:4000/graphql, and paste in the following snippet:

query {
  posts {
    title
    link
    imageUrl
  }
}

Test Mutation

To create a new Subscriptions, paste in the following snippet:

subscription {
  postAdded {
    id
    title
    link
    imageUrl
  }
}

In a separate tab, create a new Post by pasting in the following snippet:

mutation CreatePost($title: String!, $link: String!, $imageUrl: String!) {
  addPost(title: $title, link: $link, imageUrl: $imageUrl)
}

At the bottom left corner of the page, click "Query Variables" and insert the following snippet:

{
  "title": "React Native",
  "link": "https://facebook.github.io/react-native/",
  "imageUrl": "https://facebook.github.io/react-native/img/header_logo.svg"
}

Execute the mutation, and check the subscription tab to see your new post!

We now have a GraphQL server with Websockets functionality built in.

Well Done!

link Deployment

Let's deploy our app to heroku.

First we need to update our package.json:

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "start": "node src/index.js",
  "dev": "nodemon src/index.js",
  "db:reset": "npx sequelize-cli db:drop && npx sequelize-cli db:create && npx sequelize-cli db:migrate && npx sequelize-cli db:seed:all"
}

Make sure you download the heroku cli and run heroku login

  1. heroku create your-heroku-app-name
  2. heroku buildpacks:set heroku/nodejs --app=your-heroku-app-name
  3. heroku addons:create heroku-postgresql:hobby-dev --app=your-heroku-app-name
  4. git status
  5. git commit -am "add any pending changes"
  6. git push heroku master
  7. heroku run npx sequelize-cli db:migrate
  8. heroku run npx sequelize-cli db:seed:all

Having issues? Debug with the Heroku command heroku logs --tail to see what's happening on the Heroku server.

Test the endpoints!

https://your-heroku-app-name.herokuapp.com/graphql

Wrapping Up

Today we created a GraphQL subscription server. We also utilized websockets for post subscriptions. The next chapter continues with the reddit app's network requests.

format_list_bulleted
help_outline