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:
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
andDATABASE_URL
. We are going to deploy this app to Heroku. Heroku is smart enough to replaceDATABASE_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.
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
, andpostDeleted
.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 runheroku login
heroku create your-heroku-app-name
heroku buildpacks:set heroku/nodejs --app=your-heroku-app-name
heroku addons:create heroku-postgresql:hobby-dev --app=your-heroku-app-name
git status
git commit -am "add any pending changes"
git push heroku master
heroku run npx sequelize-cli db:migrate
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!
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.