Build a fullstack GraphQL app with Prisma, Apollo and Vue

In this tutorial, I will be showing you how to build a full stack GraphQL app using what I call the PAV stack (Prisma, Apollo, Vue).
Prerequisites
This tutorial assumes the following:
- Node.js and NPM installed on your computer
- The Vue CLI installed on your computer
- Basic knowledge of GraphQL
- Basic knowledge of JavaScript and Vue.js
What we’ll be building
We’ll be building techies, which is a minimalistic meetup clone. Users will be able to post tech meetups and indicate interest in attending meetups. A demo of the app is shown below:
What is Prisma?
Prisma is a real-time GraphQL database layer that turns your database into a GraphQL API, which you can either use to build your own GraphQL server or connect to it directly from the frontend. The Prisma GraphQL API provides powerful abstractions and building blocks to develop flexible, scalable GraphQL backends.
Below are some of the reasons to use Prisma:
- Type-safe API that can be used from frontend and backend, including filters, aggregations, and transactions.
- Data modeling with declarative SDL (Schema Definition Language). Prisma migrates your underlying database automatically.
- Blazing fast queries using Prisma’s query engine
- Real-time API using GraphQL Subscriptions.
- Advanced API composition using GraphQL Bindings and schema stitching.
- Works with all frontend frameworks like React, Vue.js, Angular.
With Prisma introduced, we can start building our app. This tutorial will be divided into two sections: building the server with Prisma and building the frontend with Apollo client and Vue. We’ll start with building the server.
Building the server
We need to install the Prisma CLI, which we’ll use to create our server. Run the command below to install the CLI:
$ npm install -g prisma
Because Prisma uses graphql-cli
under the hood to create the server, we need to also install the graphql-cli
:
$ npm install -g graphql-cli
With both of them installed, let’s create a new directory called techies
and change to the directory:
// create new directory
$ mkdir techies
// change to directory
$ cd techies
Now, let’s create our server using the Prisma CLI:
$ prisma init server
Select GraphQL server/full-stack boilerplate (recommended)
from the prompt, then select the node-advanced
GraphQL server boilerplate as the foundation for our server. When prompted to choose the cluster you want to deploy to, select one of the public cluster (prisma-eu1
or prisma-us1
). This will deploy our server and output the GraphQL database endpoints like below:
HTTP: https://eu1.prisma.sh/public-sprinklebow-263/server/dev
WS: wss://eu1.prisma.sh/public-sprinklebow-263/server/dev
We are only concerned with the HTTP
endpoint, as we won’t be covering GraphQL subscriptions in this tutorial.
We should now have a GraphQL server in a server
directory with two type definitions (Post
and User
) which can be found in server/database/datamodel.graphql
. The server/database/datamodel.graphql
file contains the data model that we define for our server written in SDL. We also have authentication already configured because we chose the node-advanced
GraphQL server boilerplate.
Defining the app schema
Now, let’s define the types our app will have. Update server/database/datamodel.graphql
as below:
# server/database/datamodel.graphql
type Meetup {
id: ID! @unique
organizer: User! @relation(name: "UserEvents")
title: String!
description: String!
location: String!
date: DateTime!
attendees: [User!]! @relation(name: "EventAttendees")
}
type User {
id: ID! @unique
email: String! @unique
password: String!
name: String!
myMeetups: [Meetup!]! @relation(name: "UserEvents")
meetupsAttending: [Meetup!]! @relation(name: "EventAttendees")
}
We define a new type called Meetup
with some fields, then we update the User
type to contain additional fields. We also define some relationships between the two types. A user can create many meetups, and a meetup can only be organized by a user. Hence the organizer
and myMeetups
fields on Meetup
and User
respectively. In the same vein, a meetup can have any number of attendees, and a user can attend any number of meetups. Hence the attendees
and meetupsAttending
fields on Meetup
and User
respectively.
While the server/database/datamodel.graphql
file contains the data model that we define for our server, the server/src/schema.graphql
file defines our application schema. It contains the GraphQL API that we want to expose to our frontend applications. So we need to update the server/src/schema.graphql
as well:
# server/src/schema.graphql
# The line below MUST be included
# import Meetup from "./generated/prisma.graphql"
type Query {
meetups: [Meetup!]!
meetup(id: ID!): Meetup!
me: User!
}
type Mutation {
signup(email: String!, password: String!, name: String!): AuthPayload!
login(email: String!, password: String!): AuthPayload!
createMeetup(title: String!, description: String!, date: DateTime!, location: String!): Meetup!
attending(id: ID!): Meetup!
notAttending(id: ID!): Meetup!
}
type AuthPayload {
token: String!
user: User!
}
type User {
id: ID!
email: String!
name: String!
myMeetups: [Meetup!]!
meetupsAttending: [Meetup!]!
}
NOTE: It is important to include
# import Meetup from "./generated/prisma.graphql"
at the top of the file.
Here, we define the queries we want to be performed on the server. These queries include: fetching all meetups, fetching a single meetup by its ID and fetching the authenticated user. Also, we define mutations for user sign up and log in, creating a new meetup, attending and not attending a meetup. Then we define the AuthPayload
type, which contains token
and user
fields. Lastly, we define the User
type.
Next, let’s deploy our Prisma database service:
$ cd server
$ prisma deploy
This will update the Prisma GraphQL API (server/src/generated/prisma.graphql
) that was generated initially to reflect the changes we made above.
Defining resolvers
Resolvers are defined inside the resolvers
directory. Open server/src/resolvers/Query.js
and update as below:
// server/src/resolvers/Query.js
const { getUserId } = require('../utils')
const Query = {
meetups (parent, args, ctx, info) {
return ctx.db.query.meetups({ orderBy: 'date_DESC' }, info)
},
meetup (parent, { id }, ctx, info) {
return ctx.db.query.meetup({ where: { id } }, info)
},
me (parent, args, ctx, info) {
const id = getUserId(ctx)
return ctx.db.query.user({ where: { id } }, info)
}
}
module.exports = { Query }
Here, we define resolvers for the queries we defined earlier on. We fetch all meetups that have been created and order them by their dates in a descending order. Then we fetch a meetup matching the supplied meetup ID. Lastly, using a helper getUserId
function, which expects an Authorization
header containing the authentication token (JWT) for the user. Then using the user ID gotten from the function, we fetch the authenticated user.
Next, let’s define the resolvers for our mutations. Rename server/src/resolvers/Mutation/post.js
to server/src/resolvers/Mutation/meetup.js
and update it as below:
// server/src/resolvers/Mutation/meetup.js
const { getUserId } = require('../../utils')
const meetup = {
async createMeetup (
parent,
{ title, description, date, location },
ctx,
info
) {
const userId = getUserId(ctx)
return ctx.db.mutation.createMeetup(
{
data: {
title,
description,
date,
location,
organizer: {
connect: {
id: userId
}
}
}
},
info
)
},
async attending (parent, { id }, ctx, info) {
const userId = getUserId(ctx)
const meetupExists = await ctx.db.exists.Meetup({
id
})
if (!meetupExists) {
throw new Error('Sorry, meetup not found!')
}
return ctx.db.mutation.updateMeetup(
{
where: {
id
},
data: {
attendees: {
connect: {
id: userId
}
}
}
},
info
)
},
async notAttending (parent, { id }, ctx, info) {
console.log('here')
const userId = getUserId(ctx)
const meetupExists = await ctx.db.exists.Meetup({
id
})
if (!meetupExists) {
throw new Error('Sorry, meetup not found!')
}
return ctx.db.mutation.updateMeetup(
{
where: {
id
},
data: {
attendees: {
disconnect: {
id: userId
}
}
}
},
info
)
}
}
module.exports = { meetup }
The createMeetup
mutation accepts the details of creating a new meetup. Because of the relationship we defined between a meetup and a user, we can use a connect
argument to associate or connect a meetup with an organizer (the authenticated user), we create a new meetup and return the newly created meetup. The attending
mutation accepts the ID of a meetup, then we check if the meetup exists, and throw an error if it doesn’t exist. If it does exist, we simply update the particular meetup by adding the authenticated user as an attendee. Again, we make use of the connect
argument made available due to the relationship we already defined. Lastly, the notAttending
mutation is simply the inverse of the attending
mutation. Here, we make use of the disconnect
argument, which removes the authenticated user as an attendee of the meetup.
Finally, let’s update server/src/resolvers/index.js
as below:
// server/src/resolvers/index.js
const { Query } = require('./Query')
const { auth } = require('./Mutation/auth')
const { meetup } = require('./Mutation/meetup')
const { AuthPayload } = require('./AuthPayload')
module.exports = {
Query,
Mutation: {
...auth,
...meetup
},
AuthPayload
}
Now, we can start the server and open it in Playground:
$ npm run dev
The server should be running on http://localhost:4000
. We’ll leave the server running.
Building the frontend
Now, let’s start building the frontend. For this, we’ll make use of the Vue CLI. In a new terminal window or tab, change to the project root directory (that is, techies
) and run the command below:
$ cd ..
$ vue init webpack frontend
Select Y to use Vue router. This will create a new Vue app inside a frontend
directory and install its dependencies.
Setting up Vue Apollo
Vue Apollo is an Apollo/GraphQL integration for Vue.js. We need to install it along with it necessary packages as well as the packages needed for our app:
$ cd frontend
$ npm install vue-apollo graphql apollo-client apollo-link apollo-link-http apollo-link-context apollo-cache-inmemory graphql-tag vue-moment
While those are being installed, let’s quickly go over each package:
- vue-apollo: an Apollo/GraphQL integration for Vue.js.
- graphql: a reference implementation of GraphQL for JavaScript.
- apollo-client: a fully-featured, production-ready caching GraphQL client for every server or UI framework.
- apollo-link: a standard interface for modifying control flow of GraphQL requests and fetching GraphQL results.
- apollo-link-context: used to easily set a context on your operation, which is used by other links further down the chain.
- apollo-link-http: used to get GraphQL results over a network using HTTP fetch.
- apollo-cache-inmemory: cache implementation for Apollo Client 2.0.
- graphql-tag: a JavaScript template literal tag that parses GraphQL queries.
- vue-moment: handy Moment.js filters for your Vue.js project.
Next, let’s set up the Vue Apollo plugin. Open src/main.js
and update it as below:
// src/main.js
import Vue from 'vue'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { setContext } from 'apollo-link-context'
import { InMemoryCache } from 'apollo-cache-inmemory'
import VueApollo from 'vue-apollo'
import App from './App'
import router from './router'
Vue.config.productionTip = false
// install the vue-momnet plugin
Vue.use(require('vue-moment'))
const httpLink = new HttpLink({ uri: 'http://localhost:4000/' })
const httpLinkAuth = setContext((_, { headers }) => {
// get the authentication token from localstorage if it exists
const token = localStorage.getItem('USER_TOKEN')
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
Authorization: token ? `Bearer ${token}` : ''
}
}
})
// create the apollo client
const apolloClient = new ApolloClient({
link: httpLinkAuth.concat(httpLink),
cache: new InMemoryCache()
})
// install the vue plugin
Vue.use(VueApollo)
const apolloProvider = new VueApollo({
defaultClient: apolloClient
})
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
provide: apolloProvider.provide(),
components: { App },
template: '<App/>'
})
Here, we create a new instance of httpLink
with the URL ( http://localhost:4000/
) of our GraphQL server. Then we make use of the setContext
object to create an httpLinkAuth
that gets the user token from local storage and return the headers, which contain the Authorization
header. Next, we create an Apollo client using the httpLink
and httpLinkAuth
created above and specify we want an in-memory cache. Then we install the Vue Apollo plugin, and we create a new instance of the Vue Apollo plugin using the apolloClient
created as our default client. Lastly, we make use of the apolloProvider
object by adding it in our Vue instance, the same way we would use Vue router.
You will notice we also install the vue-moment
plugin.
Adding Semantic UI
Since this tutorial is not about designs, we’ll be using Semantic UI to quickly prototype of the app. Open index.html
and update as below:
// index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Techies - A tech meetup</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.3.1/semantic.min.css">
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
We pull Semantic UI in from a CDN. Now, we can start using Semantic UI in our app. Open src/App.vue
and update as below:
// src/App.vue
<template>
<div id="app">
<nav class="ui borderless menu">
<div class="ui container">
<div class="header item">
<h1>
<router-link class="navbar-item" to="/">Techies</router-link>
</h1>
</div>
<div class="right menu">
<router-link class="ui item" to="/create">Create a Meetup</router-link>
</div>
</div>
</nav>
<div style="padding-top: 30px; padding-bottom: 30px;">
<router-view/>
</div>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
body {
background-color: #f9f9f9;
}
</style>
We define a navbar containing the app title and a link to create a new meetup. Also, we set the background color for our app.
User sign up
We’ll create a SignUp
component that will handle user sign up. Within the src/components
directory, rename HelloWorld.vue
to SignUp.vue
and update it content as below:
// src/components/SignUp.vue
<template>
<div class="ui stackable three column centered grid container">
<div class="column">
<h3 class="ui horizontal divider header">Sign Up</h3>
<form class="ui form" method="POST" @submit.prevent="signup">
<div class="field">
<label>Name</label>
<input type="text" v-model="name" required>
</div>
<div class="field">
<label>Email address</label>
<input type="email" v-model="email" required>
</div>
<div class="field">
<label>Password</label>
<input type="password" v-model="password" required>
</div>
<button class="fluid ui primary button">Sign Up</button>
</form>
<div class="ui divider"></div>
<div class="ui column grid">
<div class="center aligned column">
Already got an account? <router-link to="/login">Log In</router-link>
</div>
</div>
</div>
</div>
</template>
<script>
import { SIGNUP_MUTATION } from '@/graphql/mutations'
export default {
name: 'SignUp',
data () {
return {
name: '',
email: '',
password: ''
}
},
methods: {
signup () {
this.$apollo
.mutate({
mutation: SIGNUP_MUTATION,
variables: {
name: this.name,
email: this.email,
password: this.password
}
})
.then(response => {
localStorage.setItem('USER_TOKEN', response.data.signup.token)
// redirect to login page
this.$router.replace('/')
})
.catch(error => console.error(error))
}
}
}
</script>
This renders a simple form that collect basic details about a user and upon submission a signup
method is called. Within the signup
method, we make use of a mutate
method available on this.$apollo
(from the Vue Apollo plugin). We use the SIGNUP_MUTATION
mutation (which we’ll create shortly) and pass along the necessary variables. Once the signup process is successful, which means a user has been created, we save the user token to localstorage and redirect the user to the homepage. If there was an error, we catch and log the error to the console.
Next, let’s create the SIGNUP_MUTATION
mutation. Create a new folder called graphql
within the src
directory. Then within the newly created folder, create a new mutations.js
file and paste the code below in it:
// src/graphql/mutations.js
import gql from 'graphql-tag'
export const SIGNUP_MUTATION = gql`
mutation SignupMutation($email: String!, $password: String!, $name: String!) {
signup(email: $email, password: $password, name: $name) {
token
}
}
`
This is the GraphQL mutation that will handle creating new users on our GraphQL server. It takes the username, email, and password of a user. These variables will be passed from the SignUp
component.
To wrap up user sign up, let’s add it the /signup
route. Open src/router/index.js
and update it as below:
// src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import SignUp from '@/components/SignUp'
Vue.use(Router)
export default new Router({
mode: 'history',
linkActiveClass: 'active',
routes: [
{
path: '/signup',
name: 'SignUp',
component: SignUp
}
]
})
In addition to defining the signup route, we also set the router mode to history (which will get rid of the hash in the URL) and set the linkActiveClass
property.
User log in
Let’s add the ability for users to log in. Within src/components
, create a new LogIn.vue
file and paste the code below in it:
// src/components/LogIn.vue
<template>
<div class="ui stackable three column centered grid container">
<div class="column">
<h3 class="ui horizontal divider header">Log In</h3>
<form class="ui form" method="POST" @submit.prevent="login">
<div class="field">
<label>Email address</label>
<input type="email" v-model="email" required>
</div>
<div class="field">
<label>Password</label>
<input type="password" v-model="password" required>
</div>
<button class="fluid ui primary button">Log In</button>
</form>
<div class="ui divider"></div>
<div class="ui column grid">
<div class="center aligned column">
Don't have an account? <router-link to="/signup">Sign Up</router-link>
</div>
</div>
</div>
</div>
</template>
<script>
import { LOGIN_MUTATION } from '@/graphql/mutations'
export default {
name: 'LogIn',
data () {
return {
email: '',
password: ''
}
},
methods: {
login () {
this.$apollo
.mutate({
mutation: LOGIN_MUTATION,
variables: {
email: this.email,
password: this.password
}
})
.then(response => {
localStorage.setItem('USER_TOKEN', response.data.login.token)
this.$router.replace('/')
})
.catch(error => console.error(error))
}
}
}
</script>
This is similar to what we did with user sign up. It renders a form that accepts the user email and password, and upon submission, a login
method is called. We use the LOGIN_MUTATION
mutation. Once the login process is successful, we save the user token gotten from our GraphQL server to localstorage and redirect the user.
Next, let’s create the LOGIN_MUTATION
mutation. Paste the code below inside src/graphql/mutations.js
:
// src/graphql/mutations.js
export const LOGIN_MUTATION = gql`
mutation LoginMutation($email: String!, $password: String!) {
login(email: $email, password: $password) {
token
}
}
`
Lastly, let’s add the /login
route. Add the code below to src/router/index.js
:
// src/router/index.js
import LogIn from '@/components/LogIn'
// add these inside the `routes` array
{
path: '/login',
name: 'LogIn',
component: LogIn
},
Creating a menu component
This menu will serve as navigation for our app and it will be used across multiple pages. So let’s create a dedicate component for it. Within src/components
, create a new Menu.vue
file and paste the code below in it:
// src/components/Menu.vue
<template>
<div class="ui vertical menu">
<router-link class="item" exact to="/">All Meetups</router-link>
<template v-if="isAuthenticated">
<router-link class="item" exact to="/my-meetups">My Meetups</router-link>
<router-link class="item" exact to="/meetups-going">I'm going</router-link>
<a class="ui item" @click="logout">Logout</a>
</template>
</div>
</template>
<script>
export default {
name: 'Menu',
data () {
return {
isAuthenticated: !!localStorage.getItem('USER_TOKEN')
}
},
methods: {
logout () {
localStorage.removeItem('USER_TOKEN')
this.$router.replace('/login')
}
}
}
</script>
This renders some links depending on whether a user is logged in or not. If logged in, the user will be shown additional links to see meetups the user has created and meetups the user is attending and a link to log out. To determine whether a user is logged in or not, we define a isAuthenticated
data, which either returns true
or false
depending on if there is a USER_TOKEN
in localstorage. Lastly, we define a logout
method, which simply removes the user token from localstorage and redirect the user to the login page.
Displaying all meetups
On the app homepage, we’ll display all meetups that have been created. For this, we’ll create a new MeetupList
component. But before we do just that, let’s create a Meetup
component and paste the code below in it:
// src/components/Meetup.vue
<template>
<div class="ui divided items">
<div
class="item"
v-for="(meetup, index) in meetups"
:key="index"
>
<div class="content">
<router-link class="header" :to="`${meetup.id}`">
{{ meetup.title }}
</router-link>
<div class="meta" v-if="meetup.organizer">
Organized by <strong>{{ meetup.organizer.name }}</strong>
</div>
<div class="description">
<span>
<i class="calendar icon"></i> {{ meetup.date | moment("dddd, MMMM Do YYYY, hA") }}
</span>
<span>
<i class="map marker alternate icon"></i> {{ meetup.location }}
</span>
</div>
<div class="extra">
<i class="thumbs up icon"></i> {{ (meetup.attendees && meetup.attendees.length <= 1)
? `${meetup.attendees.length} attendee going`
: `${meetup.attendees.length} attendees going` }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Meetup',
props: ['meetups']
}
</script>
This component will represent a singular meetup on a list of meetups. It accepts an array of meetups
as props. So we loop through the array of meetups and display each of them in a list with an anchor to viewing them individually. If there is an organizer
object on the meetup, we display the meetup organizer. We use the vue-moment
plugin we installed earlier to format the meetup date. Lastly, the number of people attending an individual meetup.
Back to displaying all meetups, let’s put the Meetup
component to use. Create a new MeetupList
component and paste the code below in it:
// src/components/MeetupList.vue
<template>
<div class="ui stackable relaxed grid container">
<div class="twelve wide column">
<h2 class="ui header">All Meetups</h2>
<div class="ui segment">
<div v-if="$apollo.loading">Loading...</div>
<Meetup :meetups="meetups"/>
</div>
</div>
<div class="four wide column">
<Menu/>
</div>
</div>
</template>
<script>
import { MEETUPS_QUERY } from '@/graphql/queries'
import Menu from '@/components/Menu'
import Meetup from '@/components/Meetup'
export default {
name: 'MeetupList',
components: { Menu, Meetup },
data () {
return {
meetups: []
}
},
apollo: {
meetups: {
query: MEETUPS_QUERY
}
}
}
</script>
In addition to the Meetup
component, this meetup also makes use of the Menu
component. We define a meetups
data, which will be populated once the data is gotten from our GraphQL server. Then within the apollo
object, we define the GraphQL query to fetch all meetups. This makes use of the MEETUPS_QUERY
query. Once meetups
is populated with data from our GraphQL server, we pass it as props to the Meetup
component. You’ll also notice, we have a loader (in this case, just a text), which will be displayed while data is being fetched from the server.
NOTE: The name of our data (
meetups
in this case) must be the same name used in our GraphQL query (meetups
in this case) as defined on the GraphQL server.
Next, let’s create the MEETUPS_QUERY
query. Within the src/graphql
directory, create a new queries.js
file and paste the code below in it:
// src/graphql/queries.js
import gql from 'graphql-tag'
export const MEETUPS_QUERY = gql`
query MeetupsQuery {
meetups {
id
title
date
location
organizer {
name
}
attendees {
id
}
}
}
`
This GraphQL query fetches all the meetups that have been created on our GraphQL server.
Lastly, let’s add the /
(home) route. Add the code below to src/router/index.js
:
// src/router/index.js
import MeetupList from '@/components/MeetupList'
// add these inside the `routes` array
{
path: '/',
name: 'MeetupList',
component: MeetupList
},
Creating a new meetup
Users should be able to create meetups. Within src/components
, create a new NewMeetup.vue
file and paste the code below in it:
// src/components/NewMeetup.vue
<template>
<div class="ui stackable two column centered grid container">
<div class="column">
<h3 class="ui horizontal divider header">Create Meetup</h3>
<form class="ui form" method="POST" @submit.prevent="createMeetup">
<div class="field">
<label>Title</label>
<input type="text" v-model="title" required>
</div>
<div class="field">
<label>Location</label>
<input type="text" v-model="location" required>
</div>
<div class="field">
<label>Date</label>
<input type="datetime-local" v-model="date" required>
</div>
<div class="field">
<label>Description</label>
<textarea v-model="description" rows="10"></textarea>
</div>
<button class="ui primary button">Create Meetup</button>
</form>
</div>
</div>
</template>
<script>
import { CREATE_MEETUP_MUTATION } from '@/graphql/mutations'
import { MEETUPS_QUERY } from '@/graphql/queries'
export default {
name: 'NewMeetup',
data () {
return {
title: '',
description: '',
date: '',
location: ''
}
},
methods: {
createMeetup () {
this.$apollo
.mutate({
mutation: CREATE_MEETUP_MUTATION,
variables: {
title: this.title,
location: this.location,
date: this.date,
description: this.description
},
update: (store, { data: { createMeetup } }) => {
// read data from cache for this query
const data = store.readQuery({ query: MEETUPS_QUERY })
// add the new meetup from this mutation to existing meetups
data.meetups.push(createMeetup)
// write data back to the cache
store.writeQuery({ query: MEETUPS_QUERY, data })
}
})
.then(response => {
// redirect to home
this.$router.replace('/')
})
.catch(error => console.error(error))
}
}
}
</script>
This component renders a form for adding a new meetup. Once the form is submitted, the createMeetup
method will be called. It uses the CREATE_MEETUP_MUTATION
mutation passing to it the necessary variables. Because of Apollo client caches (in memory in our case) its queries, we need a way to update the cache whenever we perform mutations. Hence the need for the update
function, which we use to update the store by adding the newly added meetup to the cache. First, we fetch the data from the cache matching our query (MEETUPS_QUERY
), then we add the new meetup to the meetups
array. Lastly, we write the new data back to the cache. Once the meetup is added successfully, we redirect the user to the homepage.
Next, let’s create the CREATE_MEETUP_MUTATION
mutation. Paste the code below inside src/graphql/mutations.js
:
// src/graphql/mutations.js
export const CREATE_MEETUP_MUTATION = gql`
mutation CreateMeetupMutation(
$title: String!
$location: String!
$date: DateTime!
$description: String!
) {
createMeetup(
title: $title
location: $location
date: $date
description: $description
) {
id
title
date
location
organizer {
name
}
attendees {
id
}
}
}
`
Recall from our server implementation, the only authenticated user can create a new meetup. So we need a way to implement this on the frontend. Open src/main.js
and add the code below to it:
// src/main.js
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
localStorage.getItem('USER_TOKEN') ? next() : next('/login')
} else {
next()
}
})
This checks if any of our routes have a meta
object with requiresAuth
set to true
. If found, we check if there is a USER_TOKEN
in localstorage, if not found we redirect the user to the login page. Otherwise, we allow the user to continue as intended.
Lastly, let’s add the /create
route. Add the code below to src/router/index.js
:
// src/router/index.js
import NewMeetup from '@/components/NewMeetup'
// add these inside the `routes` array
{
path: '/create',
name: 'NewMeetup',
component: NewMeetup,
meta: { requiresAuth: true }
},
Here, we add the meta
object with requiresAuth
set to true
to the route.
Displaying user meetups
Now, let’s allow users to view the meetups they created. Create a new MyMeetups
component and paste the code below in it:
// src/components/MyMeetups.vue
<template>
<div class="ui stackable relaxed grid container">
<div class="twelve wide column">
<h2 class="ui header">My Meetups</h2>
<div class="ui segment">
<div v-if="$apollo.loading">Loading...</div>
<Meetup :meetups="me.myMeetups"/>
</div>
</div>
<div class="four wide column">
<Menu/>
</div>
</div>
</template>
<script>
import { ME_QUERY } from '@/graphql/queries'
import Menu from '@/components/Menu'
import Meetup from '@/components/Meetup'
export default {
name: 'MyMeetups',
components: { Menu, Meetup },
data () {
return {
me: []
}
},
apollo: {
me: {
query: ME_QUERY
}
}
}
</script>
This is pretty straightforward as it is similar to the MeetupList
component. But instead uses the ME_QUERY
query, which is used to fetch the details of the authenticated user. Then we pass the user meetups to the Meetup
component using me.myMeetups
.
Next, let’s create the ME_QUERY
query. Paste the code below inside src/graphql/queries.js
:
// src/graphql/queries.js
export const ME_QUERY = gql`
query MeQuery {
me {
id
name
myMeetups {
id
title
date
location
attendees {
id
}
}
meetupsAttending {
id
title
date
location
organizer {
name
}
attendees {
id
}
}
}
}
`
This GraphQL query is used to fetch the details of the authenticated user along with the meetups the user has created and the meetup the user is attending.
Lastly, let’s add the /my-meetups
route. Add the code below to src/router/index.js
:
// src/router/index.js
import MyMeetups from '@/components/MyMeetups'
// add these inside the `routes` array
{
path: '/my-meetups',
name: 'MyMeetups',
component: MyMeetups
},
Displaying attending meetups
For viewing meetups that a user is attending, let’s create a new MeetupsAttending
component and paste the code below in it:
// src/components/MeetupsAttending.vue
<template>
<div class="ui stackable relaxed grid container">
<div class="twelve wide column">
<h2 class="ui header">Meetups I'm Going</h2>
<div class="ui segment">
<div v-if="$apollo.loading">Loading...</div>
<Meetup :meetups="me.meetupsAttending"/>
</div>
</div>
<div class="four wide column">
<Menu/>
</div>
</div>
</template>
<script>
import { ME_QUERY } from '@/graphql/queries'
import Menu from '@/components/Menu'
import Meetup from '@/components/Meetup'
export default {
name: 'MeetupsAttending',
components: { Menu, Meetup },
data () {
return {
me: []
}
},
apollo: {
me: {
query: ME_QUERY
}
}
}
</script>
This is similar to the MyMeetups
component, but instead, we pass the meetups a user is attending to the Meetup
component.
Next, let’s add the /meetups-going
route. Add the code below to src/router/index.js
:
// src/router/index.js
import MeetupsAttending from '@/components/MeetupsAttending'
// add these inside the `routes` array
{
path: '/meetups-going',
name: 'MeetupsAttending',
component: MeetupsAttending
},
Displaying a single meetup
So far we have been able to see a list of meetups. Now, let’s allow viewing of a single meetup. Create a new SingleMeetup
and paste the code below in it:
// src/components/SingleMeetup.vue
<template>
<div class="ui stackable relaxed grid container">
<div class="twelve wide column">
<div class="ui segment">
<h1 class="ui dividing header">
{{ meetup.title }}
<div class="sub header">Organized by {{ meetup.organizer.name }}</div>
</h1>
<div class="description">
<h3 class="ui header">Details</h3>
<p>{{ meetup.description }}</p>
<p>
<span>
<i class="calendar icon"></i> {{ meetup.date | moment("dddd, MMMM Do YYYY, hA") }}
</span>
<span>
<i class="map marker alternate icon"></i> {{ meetup.location }}
</span>
</p>
</div>
<template v-if="isAuthenticated">
<h3 class="ui header">Are you going?</h3>
<button class="ui icon primary button" v-if="attending" @click="notAttendingMeetup" title="I'm not going">
<i class="large thumbs down outline icon"></i>
</button>
<button class="ui icon button" v-else @click="attendingMeetup" title="I'm going">
<i class="large thumbs up outline icon"></i>
</button>
</template>
<h3 class="ui header">
Attendees {{ `(${meetup.attendees.length})` }}
</h3>
<div class="ui bulleted list">
<div
class="item"
v-for="(attendee, index) in meetup.attendees"
:key="index"
>{{ attendee.name }}</div>
</div>
</div>
</div>
<div class="four wide column">
<Menu/>
</div>
</div>
</template>
<script>
import { MEETUP_QUERY, ME_QUERY } from '@/graphql/queries'
import {
ATTENDING_MEETUP_MUTATION,
NOT_ATTENDING_MEETUP_MUTATION
} from '@/graphql/mutations'
import Menu from '@/components/Menu'
export default {
name: 'SingleMeetup',
components: { Menu },
data () {
return {
meetup: {},
me: {},
isAuthenticated: !!localStorage.getItem('USER_TOKEN')
}
},
computed: {
attending () {
return this.meetup.attendees.some(item => {
return item.id === this.me.id
})
}
},
apollo: {
meetup: {
query: MEETUP_QUERY,
variables () {
return {
id: this.$route.params.id
}
}
},
me: {
query: ME_QUERY
}
},
methods: {
attendingMeetup () {
this.$apollo
.mutate({
mutation: ATTENDING_MEETUP_MUTATION,
variables: {
id: this.$route.params.id
}
})
.then(response => {
this.meetup = response.data.attending
})
.catch(error => console.error(error))
},
notAttendingMeetup () {
this.$apollo
.mutate({
mutation: NOT_ATTENDING_MEETUP_MUTATION,
variables: {
id: this.$route.params.id
}
})
.then(response => {
this.meetup = response.data.notAttending
})
.catch(error => console.error(error))
}
}
}
</script>
This component displays the details of a meetup along with it attendees. This makes use of the MEETUP_QUERY
query, which accepts the ID of the meetup we want to view. The ID is gotten from the route params. We need a way to pass this ID to our query. To do this, we make use of reactive parameters by defining a variables
function that returns an object containing the ID.
Also, we fetch the details of the authenticated user, which we use in the attending
computed property to determine if a user has already indicated interest in attending the meetup or not. We then use the attending
computed property to display the appropriate button. That is, if a user has already indicated interest, we display a button to cancel that, which calls a notAttendingMeetup
method. Otherwise, we display a button to indicate the interest in attending by calling a attendingMeetup
method. We make sure only authenticated users can see the buttons.
The attendingMeetup
method makes use of the ATTENDING_MEETUP_MUTATION
mutation, which simply adds the authenticated user to the meetups’ list of attendees. If the addition is successful, we update the meetup
data with that gotten from this mutation.
The notAttendingMeetup
method makes use of the NOT_ATTENDING_MEETUP_MUTATION
mutation, which simply does the inverse of the attendingMeetup
method.
Next, let’s create the MEETUP_QUERY
query. Paste the code below inside src/graphql/queries.js
:
// src/graphql/queries.js
export const MEETUP_QUERY = gql`
query MeetupQuery($id: ID!) {
meetup(id: $id) {
id
title
description
date
location
organizer {
name
}
attendees {
id
name
}
}
}
`
This GraphQL query fetches a single meetup by it ID. It takes the ID of the meetup to be fetched as an argument.
Let’s also create ATTENDING_MEETUP_MUTATION
and NOT_ATTENDING_MEETUP_MUTATION
mutations. Paste the code below inside src/graphql/mutations.js
:
// src/graphql/mutations.js
export const ATTENDING_MEETUP_MUTATION = gql`
mutation AttendingMeetupMutation($id: ID!) {
attending(id: $id) {
id
title
description
date
location
organizer {
name
}
attendees {
id
name
}
}
}
`
export const NOT_ATTENDING_MEETUP_MUTATION = gql`
mutation AttendingMeetupMutation($id: ID!) {
notAttending(id: $id) {
id
title
description
date
location
organizer {
name
}
attendees {
id
name
}
}
}
`
Lastly, let’s add the /meetups-going
route. Add the code below to src/router/index.js
:
// src/router/index.js
import SingleMeetup from '@/components/SingleMeetup'
// add these inside the `routes` array
{
path: '/:id',
name: 'SingleMeetup',
component: SingleMeetup
},
NOTE: This route should be the last route in the routes.
Let’s see our app in action
It’s time to see our app in action. Make sure the server is still running or run the command below to start the server:
$ cd server
$ npm run dev
Also, let’s get the frontend started as well:
$ cd frontend
$ npm run dev
The frontend should be running on http://localhost:8080
.
When we visit the /signup
route, we should see the sign up form as in the image below:
Similarly, when we visit the /login
route, we should see the log in form as in the image below:
Clicking the Create a Meetup link without being logged in, will redirect us to the log in form. Once we are logged in, we should see the meetup creation form as below:
Having created some meetups, the homepage (/
route)_ should now contain all the meetups that have been created:
We can view a particular meetup by clicking on the title of the meetup from the meetups list:
Also, logged in users will be able to view their meetups and the meetups they are attending.
Conclusion
In this tutorial, we looked at what Prisma is, and we also saw how to build a fullstack GraphQL app using Prisma, Apollo, and Vue. The complete code for this tutorial is available on GitHub. To learn more about Prisma, check out the Prisma docs.
May 28, 2018
Ready to begin?
Start building your realtime experience today.
From in-app chat to realtime graphs and location tracking, you can rely on Pusher to scale to million of users and trillions of messages