Professional Documents
Culture Documents
React Url Shortener
React Url Shortener
React Url Shortener
and GraphQL
Peter Jausovec
This book is for sale at http://leanpub.com/react-url-shortener
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and
many iterations to get reader feedback, pivot until you have the right book and build traction once
you do.
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i
Intended Audience . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i
Source Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i
How To Use This Book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . i
I Getting Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1. Development Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.1 Prerequisites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2 Creating the Project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Setting up GraphQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.4 Setting up Apollo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.5 Building React Components . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4. React Router . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
CONTENTS
IV User Authentication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
4.3 Login and Sign-Up React Components . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
4.4 Adding Auth Support to Graphcool Service . . . . . . . . . . . . . . . . . . . . . . . . 52
4.5 Add the template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.6 Sign me up! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4.7 Log me in! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.8 Add Authorization Token to Apollo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
4.9 Linking Data Models . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
I originally wrote the contents of this book as series of posts that were published on Medium. I
was learning about React and GraphQL and wanted to document my experiences in a form of a
tutorial that might help others as well. The book doesn’t go into a lot of details and deep technical
explanations of different parts, however it does give you enough information to get your project up
and running.
Intended Audience
The book is written as a tutorial and assumes you have basic understanding of Javascript and React.
It’s mostly meant for beginners and anyone who likes to get practical information on how to get
started with React, Apollo and GraphQL.
Source Code
Source code for the created project can be downloaded from https://github.com/peterj/shortly.
1.1 Prerequisites
We are going to use React and Apollo on the frontend and and Graphcool on the backend.
Here are the prerequisites and tools I’ll be using to work on this project:
Once you have everything, you can get started with the project!
To make sure all went well, go inside the folder that got created by the above command, and run
yarn start. If all went well, you should see the default React website open in your default browser.
Development Setup 3
1 $ cd shortly
2 $ mkdir graphcool && cd graphcool
3 $ graphcool init
4 Creating a new Graphcool service in .... �
5 Written files:
6 ├─ types.graphql
7 ├─ src
8 │ ├─ hello.js
9 │ └─ hello.graphql
10 ├─ graphcool.yml
11 └─ package.json
12 ...
• graphcool.yml — configuration file for the Graphcool service that references all types, serverless
functions and defines permissions
• types.graphql — defines the data model for the Graphcool service
• src folder — holds source code for any serverless functions
Let’s delete the src folder and remove the references to any file in that folder from the graphcool.yml
file, so the file look like this (I removed the comments as well):
1 types: ./types.graphql
2 permissions:
3 - operation: "*"
Similarly, remove the User type from the types.graphql file for now and add the Link type. This is
the type you will use to store the short links. You are going to start with a very simple model and
add more to it later. The contents of the types.graphql file should look like this:
The definition above is pretty self-explanatory — for each link we have a unique ID, a string hash,
a string URL and an optional description. If you’re wondering about what the exclamation mark
on the fields those, well it makes that field required. You can read more about modelling data in
Graphcool here.
Development Setup 5
Finally, we need to create the actual service and push the changes we made by running the deploy
command from the graphcool folder you created earlier. You’d run the same command each time
you make a change to your types or functions:
graphcool deploy
Since you are running the deploy command for the first time, you will need to make some choices.
In addition to the above shared clusters, you could also pick a custom cluster and run everything
inside a Docker container locally on your machine.
For the purpose of this book, I will pick one of the shared clusters above.
1. Pick the target environment name (default being prod). Just press enter to accept the default
value.
2. Choose the service name - I went with shortly, but feel free to use any other name.
A couple of seconds later, the CLI will give you an overview of what was added/deployed, as well
as the GraphQL endpoints called Simple API, Relay API and Subscriptions API. You will use the
Simple API endpoint to connect the React website to the GraphQL backend.
Once that’s installed, you can configure the Apollo client to connect to the GraphQL endpoint and
use a higher-order component to wrap the App component with the ApolloProvider.
2. Create the ApolloClient instance — replace the [SERVICE_ID] value with the one you got when
you deployed the Graphcool service, or just run graphcool info to show that information
again.
We are going to start with a Link and LinkList components for displaying a hardcoded (for now)
list of links.
1. Create the src/components folder and a Link.js file. The component in this file represents a
single link.
Development Setup 7
src/components/Link.js
We are passing a single link prop to the component and displaying it inside a div.
1. Create a LinkList.js file — this component is used for displaying a list of links (at this step
you will use a hardcoded array of links and once you switch over to GraphQL, you will remove
it)
Development Setup 8
src/components/LinkList.js
1 import React, { Component } from 'react';
2 import Link from './Link';
3
4 const ALL_LINKS = [
5 {
6 id: '1',
7 hash: 'ABC',
8 url: 'http://google.com',
9 description: 'Google shortlink'
10 },
11 {
12 id: '2',
13 hash: 'DEF',
14 url: 'http://graph.cool',
15 description: 'Graphcool shortlink'
16 },
17 {
18 id: '3',
19 hash: 'GHI',
20 url: 'http://reactjs.org',
21 description: 'ReactJS shortlink'
22 }
23 ];
24
25 class LinkList extends Component {
26 render() {
27 return (
28 <div>
29 {ALL_LINKS.map(link => (
30 <Link key={link.id} link={link} />
31 ))}
32 </div>
33 );
34 }
35 }
36
37 export default LinkList;
1. Finally, let’s update App.js and render the LinkList component you created in the previous
step:
Development Setup 9
src/App.js
Run yarn start from the terminal, then open the browser and navigate to http://localhost:3000 to
confirm you can see the hardcoded links as shown in the figure below.
The gql is a helper function from graphl-tag and you can use it to define the GraphQL queries.
The allLinks query name was automatically generated by Graphcool and it can be used to fetch
multiple nodes. The query name (AllLinksQuery) is the name we came up with for the query. Any
values inside the allLinks query (e.g. id, url, …) are the fields we want GraphQL to return. In the
case above we will be returning an id, url, description and a hash.
Another query that is automatically generated for our type is a Link query. With it, we can query
for a single node (an instance of a Link) by id. For example:
Queries, Mutations and Subscriptions 12
1 query SingleLink {
2 Link (id: "someid") {
3 url
4 description
5 }
6 }
Query API
For a more detailed information about the Query API, you can head over to the Query API
docs.
Now that you have the ALL_LINKS_QUERY defined, you can use the graphql container to wrap
the LinkList component with it. When query finishes executing, the results get populated to the
component’s props object. Here’s the updated code for LinkList.js:
src/components/LinkList.js
26 }
27
28 const allLinks = this.props.allLinksQuery.allLinks;
29 if (allLinks.length === 0) {
30 return <div>No links...</div>;
31 }
32
33 return (
34 <div>
35 {allLinks.map(link => (
36 <Link key={link.id} link={link} />
37 ))}
38 </div>
39 );
40 }
41 }
42
43 export default graphql(ALL_LINKS_QUERY, { name: 'allLinksQuery' })(LinkList);
Let’s explain what’s happening with the above code starting at the top.
Lines 4–16 You imported gql and graphql and defined the ALL_LINKS_QUERY.
Lines 20–31 You are using the loading and error properties on the query name prop to check if the
data was fetch or if there was an error fetching the data. In those cases you either return a simple
div with “Loading” or “Error” text. Note that these should probably be components, so you can
re-use them throughout the site. If query is done loading and there are no errors, you get the data
(allLinks) and as a final check you return a div that’s says “No links…” if there weren’t any links
returned.
Line 41 Using the graphql container, you are combining the component and the query. You are
naming the query (allLinksQuery). This name ends up being the prop name that Apollo adds to the
component.
At this point, you can navigate to http://localhost:3000 to get the “No links” message.
Let’s use the Graphcool’s playground to add a couple of links. From the command line, run graphcool
playground to open the playground. Alternatively, you can go to http://graph.cool and click the Open
Console button and then the Playground button to open the playground UI.
To create a new Link we are going to write GraphQL mutation. Copy the mutation below to the
Graphcool playground:
Queries, Mutations and Subscriptions 14
1 mutation CreateLinkMutation {
2 createLink(
3 description:"First link from GraphQL",
4 url:"http://example.com",
5 hash:"somehash") {
6 id
7 }
8 }
Just like with the queries, Graphcool creates a createLink mutation for you. You provide the values
for required fields and return an id of the created Link. Click the play button on the playground to
execute the query. The result should look something like the figure below.
Notice how the result contains the value of the id - you could also return any other field by simply
including the field name in the mutation.
Now if you go back to the React app running at http://localhost:3000 and refresh, you should see the
Loading message first and then after query loads, you will see the link you created.
In the next chapter, you will create a new React component that will be used for creating new links
using GraphQL mutation. Furthermore, you will implement a simple hash algorithm as a Graphcool
serverless function.
Let’s create a file src/components/CreateShortLink.js and come up with the UI for creating short
links. The component has two inputs, one for the URL and one for a description, and a button that
actually creates the link. Both values are stored in component state and can be retrieved when create
button is clicked.
Queries, Mutations and Subscriptions 16
src/components/CreateShortLink.js
The main logic for creating the links will be in the createShortLink function. From there, the
Queries, Mutations and Subscriptions 17
1 mutation CreateLinkMutation {
2 createLink(
3 description:"First link from GraphQL",
4 url:"http://example.com",
5 hash:"somehash") {
6 id
7 }
8 }
This time, instead of hardcoding the values, we need a way to provide the values to the mutation
that we are using. To declare variables in a mutation (or a query), you simply prepend a $ sign to
the name of the variable like this: $myvariable.
Let’s take the above mutation and refactor it to use the variables:
Inside the parenthesis next to the mutation name we declared the required string variables — later
on, we will remove the hash variable as we want to create the hashes using a serverless function.
These values get passed to the createLink mutation and when mutation executes, it returns the id
that represents the created short link.
Also, just like with the queries, we can reference this mutation when we wrap our component in
the graphql container like this:
Queries, Mutations and Subscriptions 18
To access and call the mutation, Apollo injects the mutation name into the props objects — you need
to call it and provide the values for those three variables. Before you do that though, you need to
implement the hashing function to use for creating short links. I am not going into details about the
actual hashing algorithm. Instead, we are going to use a unique id (in our case it is going to be the
total count of links) and convert it to a base 62 encoding to get the hash digits. Finally, we translate
those digits into a unique hash string.
13 789`.split(
14 ''
15 );
16 // Convert hashDigits to base62 representation
17 let hashString = '';
18 let i = 0;
19 while (hashDigits.length > i) {
20 hashString += alphabetArray[hashDigits[i]];
21 i++;
22 }
23 return hashString;
24 };
Since we need to pass the variable itemCount to the hashing algorithm, we need a way to
get that value using GraphQL. Luckily for us, Graphcool automatically implements aggregation
queries for all types in the schema. The aggregation queries are named following this format
_all[TYPE_NAME]Meta where TYPE_NAME is the name of your type (Link in our case). A query to
get the count of all links would look like this:
1 query GetLinkCountQuery {
2 links: _allLinksMeta {
3 count
4 }
5 }
Notice the name links? You can use that variable to store the result of the query so that you can
refer to it in the code. Without that, the name of the prop would be _allLinksMeta.
There are two ways we can obtain the link count — we can follow the same pattern we did when
executing the queries in the previous post — basically, execute the query, check for errors and if the
query is done loading and then return. The second option is that we could execute the query on
demand when we need the value. We are going with the latter in this case, to demonstrate how to
do that.
So, instead of using graphql container to wrap the component, we can use withApollo container
that injects the Apollo client instance as a prop to the wrapped container similarly, as it injects the
queries and mutations.
Let’s bring this all together — the hashing code, import withApollo from react-apollo, add the link
count query and take care of the component wrapping:
Queries, Mutations and Subscriptions 20
src/components/CreateShortLink.js
1 import React, { Component } from 'react';
2
3 import gql from 'graphql-tag';
4 import { graphql, withApollo } from 'react-apollo';
5
6 const CREATE_SHORT_LINK_MUTATION = gql`
7 mutation CreateLinkMutation($url: String!, $description: String!) {
8 createLink(url: $url, description: $description) {
9 id
10 }
11 }
12 `;
13
14 const GET_LINK_COUNT_QUERY = gql`
15 query GetLinkCountQuery {
16 links: _allLinksMeta {
17 count
18 }
19 }
20 `;
21
22 const createHash = itemCount => {
23 let hashDigits = [];
24 // dividend is a unique integer (in our case, number of links)
25 let dividend = itemCount + 1;
26 let remainder = 0;
27 while (dividend > 0) {
28 remainder = dividend % 62;
29 dividend = Math.floor(dividend / 62);
30 hashDigits.unshift(remainder);
31 }
32 console.log(hashDigits);
33 const alphabetArray = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456\
34 789`.split(
35 ''
36 );
37 // Convert hashDigits to base62 representation
38 let hashString = '';
39 let i = 0;
40 while (hashDigits.length > i) {
41 hashString += alphabetArray[hashDigits[i]];
42 i++;
Queries, Mutations and Subscriptions 21
43 }
44 return hashString;
45 };
46
47 class CreateShortLink extends Component {
48 constructor(props) {
49 super(props);
50 this.state = {
51 description: '',
52 url: ''
53 };
54 }
55
56 createShortLink = async () => {
57 // ADD CODE HERE!
58 };
59
60 render() {
61 return (
62 <div>
63 <input
64 id="url"
65 type="text"
66 value={this.state.url}
67 placeholder="Link URL"
68 onChange={e => this.setState({ url: e.target.value })}
69 />
70 <input
71 id="description"
72 type="text"
73 value={this.state.description}
74 placeholder="Link description"
75 onChange={e => this.setState({ description: e.target.value })}
76 />
77 <button onClick={() => this.createShortLink()}>Create</button>
78 </div>
79 );
80 }
81 }
82
83 export default graphql(CREATE_SHORT_LINK_MUTATION, {
84 name: 'createShortLinkMutation'
85 })(withApollo(CreateShortLink));
Queries, Mutations and Subscriptions 22
Now that everything is in place, we can write the code that’s going to get the values we entered, get
the link count, create a hash and finally run the mutation to create a new short link.
Next, let’s update the render method in App.js and add the CreateShortLink to it:
src/App.js
1 import React, { Component } from 'react';
2 import LinkList from './components/LinkList';
3 import CreateShortLink from './components/CreateShortLink';
4
5 class App extends Component {
6 render() {
7 return (
8 <div>
9 <div>
10 <h2>All links</h2>
11 <LinkList />
12 </div>
13 <div>
14 <h2>Create a short link</h2>
15 <CreateShortLink />
16 </div>
17 </div>
Queries, Mutations and Subscriptions 23
18 );
19 }
20 }
21
22 export default App;
Note that it’s not perfect as we need to reload the page manually each time a new link is created.
We could use the GraphQL subscriptions to handle this.
Queries, Mutations and Subscriptions 24
Next, we create a web socket link and use the Subscriptions API endpoint for the Graphcool service
(run graphcool info to get to the endpoints). With both links set, we are going to use split and
getMainDefinition functions from Apollo to check what type of operation is being used (e.g. a
query, mutation or a subscription) and return the correct link. Here’s how the split function looks
like (explained in more details here):
Once you have this in your index.js file, you can use the link variable when creating the Apollo
Client instance:
Queries, Mutations and Subscriptions 25
At this point everything should still work they way it did before — you need to create and actually
use the subscription in the LinkList component where we are showing all links.
Here is the query for subscription that triggers each time a new link is created - add this subscription
to the LinkList.js file:
As with mutations and queries, we provide a name for the subscription and define the filter we
want — in our case, we want to get back all fields when new Link object is created.
We are going to use a function called subscribeToMore on the allLinksQuery prop to subscribe to
updates. This method takes two arguments: first one is the subscription query we defined above
and the second one is an updateQuery function that gets called each time a new link is created.
This function takes the previous state and data that was sent from the subscription and we need
to merge the new data with the previous state and return the updated state. Let’s implement this
in the componentDidMount function in the LinkList component - add the code below to inside the
LinkList class in the src/components/LinkList.js file:
Queries, Mutations and Subscriptions 26
1 componentDidMount() {
2 this.props.allLinksQuery.subscribeToMore({
3 document: LINKS_SUBSCRIPTION,
4 updateQuery: (prev, { subscriptionData }) => {
5 const newLinks = [
6 ...prev.allLinks,
7 subscriptionData.data.Link.node,
8 ];
9 const result = {
10 ...prev,
11 allLinks: newLinks,
12 };
13 return result;
14 },
15 });
16 }
One last thing we need to do to make this work correctly is to update the fetchPolicy to
network-only when querying for link count in the src/components/CreateShortLink.js file. Since
we are using the subscription we don’t have to refresh the page anymore to see the updates — not
refreshing the page also means that each time we execute the link count query will get the cached
number and our hashes will be incorrect. Add the fetchPolicy to the linkCountQuery variable in
the createShortLink function inside src/components/CreateShortLink.js:
Network-only option tells Apollo to not use the cached values and always re-query. We will use the
same policy once we add authentication.
Ok, let’s try this out now! Go to http://localhost:3000 and see that as soon as you add a link, the list
of links gets updated automatically.
III Serverless Functions
This chapter talks about different function types that Graphcool service supports and explains how
to use them for implementing our hashing function.
3. Creating Serverless Hashing
Function
In this chapter we are going to take the hashing function we implemented in the previous post and
put it into a Graphcool function.
Functions get defined in the Graphcool service definition file (graphcool.yml). Graphcool supports a
couple of different function types that can be invoked either as webhooks or as managed functions.
3.2 Subscriptions
Subscription requires a subscription query that determines when the function should be triggered
(e.g. when an object gets created, updated or deleted). The backing function gets executed after the
mutation happens. An example of this could be if we wanted to send a welcome email each time a
new user signs-up.
3.3 Resolvers
Resolvers are the most powerful types as they allow you to come up with a new mutation and/or
query definition. We will use resolvers in the upcoming chapter where we will create a mutation
for signing up and logging in users as well as a query that returns a logged in user.
1 subscription NewLinkCreatedSubscription {
2 Link(filter: { mutation_in: [CREATED] })
3 node {
4 id
5 }
6 }
7 }
In the handler function, we’d get the data returned from the subscription (id), next we’d get the
count of all links, create a hash and finally update the existing Link with this mutation:
1 ~~~~~~~~
2 subscription {
3 Link(filter: { mutation_in: [CREATED] }) {
4 node {
5 id
6 }
7 }
8 }
9 ~~~~~~~~
3. Create a graphcool/src/createShortLink.js file that fires off each time a new link is created.
graphcool/src/createShortLink.js
21 hashString += alphabetArray[hashDigits[i]];
22 i++;
23 }
24 return hashString;
25 };
26
27 module.exports = async event => {
28 // Get the data from the event - the data
29 // is determined by the subscription. In our case, it will look like this:
30 // event = {
31 // "data": {
32 // "Link": {
33 // "node": {
34 // "id": "LINK_ID"
35 // }
36 // }
37 // }
38 // }
39 const { id } = event.data.Link.node;
40
41 const graphcool = fromEvent(event);
42 const api = graphcool.api('simple/v1');
43
44 // 1. Get the link count.
45 const getLinkCountQuery = `
46 query GetLinkCountQuery {
47 links: _allLinksMeta {
48 count
49 }
50 }`;
51
52 const linkCountQueryResult = await api.request(getLinkCountQuery);
53 const linkCount = linkCountQueryResult.links.count;
54
55 // 2. Get the hash.
56 const hash = createHash(linkCount);
57
58 // 3. Update the link with a hash.
59 const updateLinkMutation = `
60 mutation ($id: ID!, $hash: String!) {
61 updateLink(id: $id, hash: $hash) {
62 id
63 }
Creating Serverless Hashing Function 32
64 }`;
65
66 const variables = { id, hash };
67 await api.request(updateLinkMutation, variables);
68
69 return {
70 data: {
71 success: true
72 }
73 };
74 };
1. Install the dependencies for subscription function by running the following command from the
graphcool folder where package.json file is:
2. Update the graphcool.yml to reference the .graphql and .js file we created:
1 functions:
2 createShortLink:
3 type: subscription
4 query: src/createShortLink.graphql
5 handler:
6 code: src/createShortLink.js
3. Deploy the changes to create the subscription by running graphcool deploy from the
graphcool folder:
1 $ graphcool deploy
2 ...
3 Subscription Functions
4 createShortLink
5 + A new subscription with the name `createShortLink` is created.
There are a couple of small things we need to take care of before we try out the subscription:
1. Remove the createHash function, $hash variable from the mutation and GET_LINK_COUNT_QUERY
from src/components/CreateShortLink.js. Here’s how the updated file should look like:
Creating Serverless Hashing Function 33
src/components/CreateShortLink.js
1 import React, { Component } from 'react';
2
3 import gql from 'graphql-tag';
4 import { graphql, withApollo } from 'react-apollo';
5
6 const CREATE_SHORT_LINK_MUTATION = gql`
7 mutation CreateLinkMutation($url: String!, $description: String!) {
8 createLink(url: $url, description: $description) {
9 id
10 }
11 }
12 `;
13
14 class CreateShortLink extends Component {
15 constructor(props) {
16 super(props);
17 this.state = {
18 description: '',
19 url: ''
20 };
21 }
22
23 createShortLink = async () => {
24 const { url, description } = this.state;
25 await this.props.createShortLinkMutation({
26 variables: {
27 url,
28 description
29 }
30 });
31 };
32
33 render() {
34 return (
35 <div>
36 <input
37 id="url"
38 type="text"
39 value={this.state.url}
40 placeholder="Link URL"
41 onChange={e => this.setState({ url: e.target.value })}
42 />
Creating Serverless Hashing Function 34
43 <input
44 id="description"
45 type="text"
46 value={this.state.description}
47 placeholder="Link description"
48 onChange={e => this.setState({ description: e.target.value })}
49 />
50 <button onClick={() => this.createShortLink()}>Create</button>
51 </div>
52 );
53 }
54 }
55
56 export default graphql(CREATE_SHORT_LINK_MUTATION, {
57 name: 'createShortLinkMutation'
58 })(withApollo(CreateShortLink));
1. Make the hash field in types.graphql optional, by removing the exclamation mark:
2. Run the graphcool deploy command again to update the type and then you are ready to test
it out!
Open up the web site (yarn start) if you don’t have it running already and try to create a new
link — everything should work exactly like before, the only difference is that the hashing function is
now being invoked as part of a subscription on the Link. To really make sure that this works, you
can run graphcool logs to get the invocation logs from your function(s). A sample log should looks
like this:
Creating Serverless Hashing Function 35
If there are any errors that occur during function invocation, you should be able to see them from
the logs as well.
Another helpful command for testing the functions is the graphcool invoke-local command from
the CLI. Instead of going through the web site to create new links, you can directly send a JSON
payload to the function like this:
Creating Serverless Hashing Function 37
1 {
2 "data": {
3 "Link": {
4 "node": {
5 "id": "LINK_ID"
6 }
7 }
8 }
9 }
This way you can test your function for bunch of scenarios that might be hard to test through the
web site — e.g. invalid payloads, non-existent link IDs, etc.
In the next chapter we will work on getting some short URL stats — number of clicks. In order to do
that, we will have to come up with a handler for links. For example if we generate hash “ABC123”, we
will need to write some code that’s going to execute when user visits “http://localhost:3000/ABC123”,
and translate that hash to an actual link and redirect the user to it. Once we have the handler we
can start collecting click stats.
4. React Router
With the way project is currently, we can list and create short URLs, but we can’t do anything with
those short links yet. In this post we will set up routes to handle short links as well as start tracking
the number of clicks and updating the link list to show number of clicks in real time. Let’s get started!
To make our code more readable, we are going to create a new file called src/AppRouter.js where
we implement our routing logic. In this file we are going to declare which component should render,
based on the URL that was requested. For example, if you navigate to root http://localhost:3000/ we
should render the App.js component. Since App.js will serve as our main/home page, let’s rename
it to Home.js - don’t forget to also rename the class name from App to Home.
Here’s how the AppRouter.js file with simple routing logic looks like:
React router has two types of routers: a BrowserRouter and a HashRouter. The main difference
between the two is in a way they generate URLs. For example, this would be a BrowserRouter URL:
http://localhost:8080/login, while a HashRouter URL would look like this http://localhost:8080/#/login.
React Router 39
Inside of the BrowserRouter we define a single Route that renders some UI (component={Home}), if lo-
cation matches the routes path (path="/"). To put it even simpler: if I go to http://localhost:3000/,
router will render the Home component. For example, path with value /home translates to to
http://localhost:3000/home URL. Since BrowserRouter only supports a single child, we are going
to use a Switch later on.
To make use of the AppRouter we created, replace the <App /> component in index.js with
AppRouter:
1 ReactDOM.render(
2 withApolloProvider(<AppRouter />),
3 document.getElementById('root'),
4 );
If you run yarn start and go to http://localhost:3000/ you should get the exact same view as
before.
In order to handle short URLs and resolve them, we need to define another Route. So, if user
visits http://localhost:3000/shorturl, we need to get the shorturl part of the URL, figure out
the expanded URL by making a GraphQL query and redirecting the user to the full URL. If we don’t
find the full URL or if there’s an error, we will show a simple error message.
Since the short URLs are going to be dynamic, we will use a variable in the URL path to capture the
short URL hash. If the router matches the URL to this route, we are going to extract the shortUrl
and render a simple component that’s going to do a lookup and redirect to the full URL. Let’s create
a file called src/components/ShortLinkRedirect.js and implement the component. Here’s the code
for that component:
src/components/ShortLinkRedirect.js
Let’s explain what’s happening in the code above. First, we are creating a new GraphQL query that
takes a $hash as an input and returns us an URL that matches that hash. In the ShortLinkRedirect
component we are passing in the hash and a couple of properties from the data object (this gets
injected by the graphql container on line 31). Similarly as in previous components, we do the
following (lines 15–28):
A thing to note on line 36 is how we’re extracting hash variable that’s being passed to the component
from the AppRouter.js file.
That said, we should define this /:hash route in the AppRouter.js file. Update AppRouter instance
like this:
React Router 41
With :hash we are indicating that we want to capture the URL parameter — we read that parameter
from props.match.params object and send it as a prop to ShortLinkRedirect, where it gets used as
an input variable to the GET_FULL_LINK_QUERY.
Try going to the web site and creating either a new short link or accessing a short link you’ve created
before. Anytime you visit http://localhost:3000/[HASH] we try to figure out the long URL for the
provided hash and redirect appropriately.
We created a stats field on Link type and added the @relation directive to define a relation to the
LinkStats type. We do a similar thing on the LinkStats type and we connect the field link back to
the Link type.
Let’s deploy these changes to Graph.cool by running graphcool deploy and then we can update the
code and queries to increment the click count and display the click counts in LinkList component.
With service changes deployed, let’s update the LinkList.js component first.
1. Update the ALL_LINKS_QUERY to include the number of clicks and an ID in the results:
2. Update the render method in Link.js to get the number of clicks and show it next to the link:
React Router 43
1 render() {
2 const clickCount =
3 (this.props.link.stats && this.props.link.stats.clicks) || 0;
4 return (
5 <div>
6 <div>
7 {this.props.link.description} (<a
8 href={this.props.link.hash}
9 >
10 {this.props.link.hash}
11 </a>) --> clicks: {clickCount}
12 </div>
13 </div>
14 );
15 }
If you refresh the web site at this point, you should see the changes we added to the UI with number
of clicks on each link being 0. On to updating the code that increments the link count! First, we
need a simple mutation that’s going to take the id of the link and update the click count. Open the
ShortLinkRedirect.js and follow the steps below:
1. Update the GET_FULL_LINK_QUERY to return the id field and the clicks field as well:
3. Create the CREATE_LINK_STATS_MUTATION for creating new link stats (i.e. first time someone
clicks on a short link):
This should look familiar to previous queries we did — we are passing in the variables and calling
updateLink mutation to update the clicks. And in the second mutation we are creating new links
stats for the provided link.
1. Add another graphql container to the export statements and wrap them with compose:
src/components/ShortLinkRedirect.js
1 import React from 'react';
2 import gql from 'graphql-tag';
3 import { graphql, compose } from 'react-apollo';
4 import PropTypes from 'prop-types';
5
6 const GET_FULL_LINK_QUERY = gql`
7 query GetFullLink($hash: String!) {
8 allLinks(filter: { hash: $hash }) {
9 id
10 url
11 stats {
12 id
13 clicks
14 }
15 }
16 }
17 `;
18
19 const UPDATE_CLICK_COUNT_MUTATION = gql`
20 mutation UpdateClickCount($id: ID!, $clicks: Int!) {
21 updateLinkStats(id: $id, clicks: $clicks) {
22 id
23 }
24 }
25 `;
26
27 const CREATE_LINK_STATS_MUTATION = gql`
28 mutation CreateLinkStats($linkId: ID!, $clicks: Int!) {
29 createLinkStats(linkId: $linkId, clicks: $clicks) {
30 id
31 }
32 }
33 `;
34
35 const ShortLinkRedirect = ({
36 updateClickCount,
37 createLinkStats,
38 hash,
39 data: { loading, error, allLinks }
40 }) => {
41 if (error) {
42 return <div>Error occurred: {error}</div>;
React Router 46
43 }
44
45 if (loading) {
46 return <div>Loading ...</div>;
47 }
48
49 if (!allLinks || allLinks.length !== 1) {
50 return <div>No redirect found for '{hash}'</div>;
51 }
52
53 const linkInfo = allLinks[0];
54
55 if (!linkInfo.stats) {
56 // Create new link stats
57 createLinkStats({
58 variables: {
59 linkId: linkInfo.id,
60 clicks: 1
61 }
62 });
63 } else {
64 let currentClicks = (linkInfo.stats && linkInfo.stats.clicks) || 0;
65
66 // Increment the click count
67 currentClicks++;
68
69 // Update the click count.
70 updateClickCount({
71 variables: {
72 id: linkInfo.stats.id,
73 clicks: currentClicks
74 }
75 });
76 }
77
78 // Navigate to the full URL
79 window.location = linkInfo.url;
80 return null;
81 };
82
83 ShortLinkRedirect.propTypes = {
84 hash: PropTypes.string
85 };
React Router 47
86
87 export default compose(
88 graphql(UPDATE_CLICK_COUNT_MUTATION, { name: 'updateClickCount' }),
89 graphql(CREATE_LINK_STATS_MUTATION, { name: 'createLinkStats' }),
90 graphql(GET_FULL_LINK_QUERY, {
91 options: ({ hash }) => ({ variables: { hash } })
92 })
93 )(ShortLinkRedirect);
At this point you can open http://localhost:3000 and you will see how the counter updates each
time a link is clicked.
IV User Authentication
This part talks about implementing user authentication - login and sign-up functionality - using
React and GraphQL mutations and queries.
Login Component
Create the src/components/Login.js file with the following contents (we will come back in a bit to
implement the login function and wrap the component with necessary containers):
src/components/Login.js
20 <input
21 id="email"
22 type="text"
23 value={this.state.email}
24 placeholder="Email address"
25 onChange={e => this.setState({ email: e.target.value })}
26 />
27 <br />
28 <input
29 id="password"
30 type="password"
31 value={this.state.password}
32 placeholder="Password"
33 onChange={e => this.setState({ password: e.target.value })}
34 />
35 <br />
36 <button onClick={() => this.login()}>Login</button>
37 </div>
38 );
39 }
40 }
41
42 export default Login;
This will give us a very basic looking login component as shown below:
50
Sign-Up Component
The sign-up component looks pretty much the same as login:
src/components/Signup.js
17 return (
18 <div>
19 <h2>Join Shortly</h2>
20 <input
21 id="email"
22 type="text"
23 value={this.state.email}
24 placeholder="Email address"
25 onChange={e => this.setState({ email: e.target.value })}
26 />
27 <br />
28 <input
29 id="password"
30 type="password"
31 value={this.state.password}
32 placeholder="Password"
33 onChange={e => this.setState({ password: e.target.value })}
34 />
35 <br />
36 <button onClick={() => this.signup()}>Signup</button>
37 </div>
38 );
39 }
40 }
41
42 export default Signup;
In order to render these two new components, we need to slightly modify the AppRouter to say
that whenever someone requests /login or /signup path, we are going to render the corresponding
components. After you’ve imported both Login and Signup components to the AppRouter.js, add
the following two routes below the Home component route:
Start up yarn (yarn start) and navigate to /login and /signup to see the components render.
Now that we have the components in place, let’s go to the backend and bring in the email/password
auth.
52
Signup
For signup we define a new mutation called signupUser that has this signature:
signupUser(email: String!, password: String!): SignupUserPayload
This is the mutation we are calling when user tries to sign up for an account. What happens as part
of this mutation is defined by our function that we define (similarly as we did in previous post where
we defined a function that handled a subscription). Here’s what the signup function does:
In the end, it returns an ID of a newly created user and an a token we will store locally. In addition
to the things above, this would be the place where we could add things such as sending a welcome
email, or making user confirm their email address etc.
Authenticate
Authenticate is another new mutation we define and it’s very similar in signature to the signup
mutation:
authenticateUser(email: String!, password: String!): AuthenticateUserPayload
This is the mutation that gets called when user is trying to log in to our website. As part of this
mutation we are going to check if the user with the provided email exists, and if it does, we ensure
the passwords match then finally generate token and return it.
53
Logged-in User
The last part of the template is a query that we can execute and it’s going to return us user ID
if the request itself contains a valid token. The idea is that we are going to execute this query on
components that require authentication and then before rendering the component we can check if
we got any user data back or not — if we didn’t we can redirect the user to the login page, and if we
did, we can continue and render the component.
Above command will pull down the .graphql and .ts files that represent the mutations and
the functions we explained above. We still need to manually update the types.graphql and
graphcool.yml files to include these in our service. If you open any of those two files, you will
see that some commented out declarations were added to it — this is Graphcool CLI helping us to
define the functions and/or models.
Go ahead and uncomment the signup, authenticate and logeddInUser functions in graphcool.yml
as well as the User model in the types.graphql file.
Note: we will come back to the types.graphql file shortly and add the relation between User and
Link models, so don’t worry about that at the moment.
Let’s deploy the changes and make sure all is good — run graphcool deploy and after a couple of
seconds you should get the list of resolver functions and types that were created.
Next, let’s implement sign up and login functionality and then as a last step we will hook up the
relation between Links and Users.
Finally, we need to implement the signup function. Here’s the full implementation:
There’s nothing too complicated in the code above — we are calling the mutation then taking the ID
and token and storing it in the local storage. Finally, we redirect the user to home page.
Note: you probably noticed the lack of proper error handling — writing errors blindly to the console is
not good — make sure you always parse the errors and show the sanitized error message , instead of
full errors that could include a call stack and possibly information that shouldn’t be ever shared. To
fix this, you could add an error property to the state and update it with a generic error such as ‘Sign
up failed — try again’.
55
We can try out the signup now and see if it works — navigate to /signup and try entering an email
and a sample password — if all is good, you should get redirected to the home page. You can also
check that the user information was stored in the Graphcool service (tip: run graphcool playground
and click on Data).
Finally, implement the login function that’s pretty much identical with the sign up function
(difference being the mutation we call):
14 result.data.authenticateUser.token,
15 );
16 this.props.history.push('/');
17 } catch (err) {
18 // TODO: Handle errors properly here.
19 }
20 };
Try this out by going to the /login page and using the same email/password you used to sign up. If
you get redirected to the home page, it worked!
Issue we have to solve next is the fact that regardless if you’re logged in or not, when you navigate
to the home page, you’ll always get the list of links and ability to create links. We should change
that, so if you’re not logged in, we render something that gives you links to login and sign up page,
and if you are logged in, we will show you the links.
We are defining an apolloLinkWithToken in the code above (lines 1–10), then reading our SHORTLY_-
TOKEN from the local storage, creating the auth header with the token (or null if token is not there) and
setting it as an authorization header. Finally, we call the forward function and pass in the updated
operation (with the authorization header) — this continues with the chain of calls and eventually the
request goes out to the server. Lastly, we need to update the link we are using in line 20 to be the
httpLinkWithToken we defined in line 12.
At this point, if we are logged in and we try to call the loggedInUser query we will get back the ID
of the logged in user. Let’s add this functionality to the Home.js component. First, this is the query
we are going to use:
Notice that we don’t need to provide any parameters to the above query, but when the function
behind the query runs, it will check if there was an authorization header attached to it and figure
out which user ID corresponds to that token and return us the ID of that user (or nothing, if there
was no or invalid auth header provided).
Let’s wrap the component:
export default graphql(LOGGED_IN_USER_QUERY, { name: 'currentUser' })(Home);
And update the render method to render different UI based on the fact if user is logged in or not
as well as implement the logout functionality. Here’s the full Home.js file with all these things
implemented:
58
src/Home.js
1 import React, { Component } from 'react';
2 import LinkList from './components/LinkList';
3 import CreateShortLink from './components/CreateShortLink';
4 import gql from 'graphql-tag';
5 import { graphql } from 'react-apollo';
6
7 const LOGGED_IN_USER_QUERY = gql`
8 query CurrentUser {
9 loggedInUser {
10 id
11 }
12 }
13 `;
14
15 class Home extends Component {
16 logout = () => {
17 localStorage.removeItem('SHORTLY_ID');
18 localStorage.removeItem('SHORTLY_TOKEN');
19 this.props.history.push('/');
20 };
21
22 render() {
23 if (this.props.currentUser && this.props.currentUser.loading) {
24 return <div>Loading ... </div>;
25 }
26
27 const userId =
28 this.props.currentUser.loggedInUser &&
29 this.props.currentUser.loggedInUser.id;
30
31 if (userId) {
32 return (
33 <div>
34 Hi user <b>{userId}</b> (
35 <button onClick={() => this.logout()}>logout</button>)
36 <br />
37 <div>
38 <h2>Create a short link</h2>
39 <CreateShortLink />
40 </div>
41 <div>
42 <h2>All links</h2>
59
43 <LinkList />
44 </div>
45 </div>
46 );
47 } else {
48 return (
49 <div>
50 Please <a href="/login">login</a> or <a href="/signup">sign up</a>!
51 </div>
52 );
53 }
54 }
55 }
56
57 export default graphql(LOGGED_IN_USER_QUERY, { name: 'currentUser' })(Home);
In the render method we wait for the query to execute and then try to get the userId from the
query (line 27). Finally, we use a simple if statement to check if we got the user id, then render the
component with the logout button and the list of links, otherwise we render a div with links to login
and sign up page. Figure below shows the view with login and sign up links as well as the view
when user is logged in.
60
This looks much better (from the functional standpoint, not the design standpoint). Last thing left is
to actually add the relation between User and Link models and modify the queries a bit, so we only
display links from the logged in users.
The above changes are adding a createdBy field to each link that points to the User that created the
link, and a field in User model that gives us an array of all links that the user created.
Since schema changes involve new relations with required values, I am suggesting you delete all
links and users you’ve created so far testing this, then run graphcool deploy to deploy the updated
schema. Alternatively, you can run graphcool deploy --force.
We can start updating the queries now. Let’s start with src/components/CreateShortLink.js and
CREATE_SHORT_LINK_MUTATION by including the createdBy field in the query:
Next, we can pass the user ID we read from the local storage to the mutation in createShortLink
function:
63
1 await this.props.createShortLinkMutation({
2 variables: {
3 url,
4 description,
5 createdById : localStorage.getItem('SHORTLY_ID')
6 },
7 });
Note: we could have also added the loggedInUser query to the component and use the user ID
from that query to create a link. An even better solution would probably be to create a container
called LoggedInContainer and use that to wrap all components that we want to use when user is
logged in. That way, we do the loggedInUser query on the container level and not separately in each
component.
Since we added the createdById field to the filter, we need to update the export statement to pass
the id from the local storage to the ALL_LINKS_QUERY like this:
Also, we need to update the filter on our subscription, so it only fires when the user currently logged-
in creates or updates a link. Here’s the updated filter in LINKS_SUBSCRIPTION variable:
64
We also need to pass the createdById variable to the subscription when we call subscribeToMore
function in the componentDidMount. Here’s the line to add after the document property:
variables: { createdById: localStorage.getItem('SHORTLY_ID') }
Similarly as with previous queries, we read the user ID from the local storage and pass it as a
createdById variable to the document (GraphQL query).
At this point you can create multiple users, create links for each of them and verify that you only
see the links from the logged in user.
About The Author
Peter Jausovec is a software engineer with more than ten years of experience in the field of software
development and tech. During his career, he spent time in various roles, starting with QA then
moving to software engineering and leading tech teams. His early career was mostly focused on
developer and cloud tooling; however, in recent years he has been focusing on developing distributed
systems cloud-native solutions.
You can can contact him on Twitter.