Table of Contents
- Prerequisites
- Project Overview
- Setting Up the GraphQL Backend
- Setting Up the Vue.js Frontend
- Building Vue Components
- 5.1 Home Page (Post List)
- 5.2 Post Detail Page
- 5.3 Create Post Page
- Integrating Apollo Client with Vue
- Handling Authentication (Optional)
- Deployment
- Conclusion
- References
Prerequisites
Before starting, ensure you have the following tools installed:
- Node.js (v14+ recommended) and npm/yarn
- Basic knowledge of JavaScript and Vue.js (Vue 3 preferred)
- Familiarity with GraphQL concepts (queries, mutations, schemas)
- A code editor (e.g., VS Code)
Project Overview
Our blog will have two main parts:
- GraphQL Backend: A server that defines the data schema, handles queries/mutations, and connects to a database. We’ll use Apollo Server for this.
- Vue.js Frontend: A single-page application (SPA) with pages to list posts, view post details, and create new posts. We’ll use Vue 3 with the Composition API and Apollo Client for data fetching.
Setting Up the GraphQL Backend
3.1 Initializing the Backend
First, create a new directory for your backend and initialize a Node.js project:
mkdir blog-backend && cd blog-backend
npm init -y
Install required dependencies:
npm install apollo-server graphql prisma @prisma/client cors dotenv
npm install --save-dev prisma typescript ts-node @types/node
3.2 Defining the GraphQL Schema
The schema defines the data types and operations (queries/mutations) available in your API. Create a schema.js file in the root of blog-backend:
// schema.js
const { gql } = require('apollo-server');
const typeDefs = gql`
type Post {
id: ID!
title: String!
content: String!
createdAt: String!
}
type Query {
posts: [Post!]!
post(id: ID!): Post
}
type Mutation {
createPost(title: String!, content: String!): Post!
updatePost(id: ID!, title: String, content: String): Post
deletePost(id: ID!): Boolean
}
`;
module.exports = typeDefs;
Here, we define a Post type with fields id, title, content, and createdAt. Queries let us fetch all posts or a single post by ID. Mutations handle creating, updating, and deleting posts.
3.3 Setting Up Resolvers
Resolvers are functions that “resolve” the data for each field in your schema. Create a resolvers.js file:
// resolvers.js
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const resolvers = {
Query: {
posts: async () => {
return await prisma.post.findMany({ orderBy: { createdAt: 'desc' } });
},
post: async (_, { id }) => {
return await prisma.post.findUnique({ where: { id } });
},
},
Mutation: {
createPost: async (_, { title, content }) => {
return await prisma.post.create({
data: { title, content, createdAt: new Date().toISOString() },
});
},
updatePost: async (_, { id, title, content }) => {
return await prisma.post.update({
where: { id },
data: { title, content },
});
},
deletePost: async (_, { id }) => {
await prisma.post.delete({ where: { id } });
return true;
},
},
};
module.exports = resolvers;
3.4 Connecting to a Database
We’ll use Prisma as an ORM to interact with a SQLite database (simple for development). Initialize Prisma:
npx prisma init
This creates a prisma directory with a schema.prisma file. Update schema.prisma to define the Post model:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Post {
id String @id @default(uuid())
title String
content String
createdAt String
}
Run the migration to create the database table:
npx prisma migrate dev --name init
3.5 Starting the GraphQL Server
Create an index.js file to start the Apollo Server:
// index.js
const { ApolloServer } = require('apollo-server');
const typeDefs = require('./schema');
const resolvers = require('./resolvers');
const server = new ApolloServer({
typeDefs,
resolvers,
cors: {
origin: '*', // Allow all origins (for development only)
},
});
server.listen().then(({ url }) => {
console.log(`GraphQL server running at ${url}`);
});
Start the server:
node index.js
Visit http://localhost:4000 in your browser to access Apollo Studio, where you can test queries/mutations (e.g., run a posts query to fetch all posts).
Setting Up the Vue.js Frontend
4.1 Creating a Vue App with Vue CLI
If you don’t have Vue CLI installed, run:
npm install -g @vue/cli
Create a new Vue 3 project:
vue create blog-frontend
Select “Default (Vue 3)” or manually choose features (e.g., TypeScript, Vue Router).
4.2 Installing Dependencies
Navigate to the frontend directory and install Apollo Client and GraphQL:
cd blog-frontend
npm install @apollo/client graphql vue-router@4
4.3 Configuring Vue Router
Set up routes for the blog pages. Create a src/router/index.js file:
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import PostDetail from '../views/PostDetail.vue';
import CreatePost from '../views/CreatePost.vue';
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/posts/:id', name: 'PostDetail', component: PostDetail, props: true },
{ path: '/create', name: 'CreatePost', component: CreatePost },
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
Update src/main.js to use the router:
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
createApp(App).use(router).mount('#app');
Building Vue Components
5.1 Home Page (Post List)
Create src/views/Home.vue to display a list of posts:
<!-- src/views/Home.vue -->
<template>
<div class="home">
<h1>My Blog</h1>
<div class="post-list">
<PostCard v-for="post in posts" :key="post.id" :post="post" />
</div>
<router-link to="/create" class="create-btn">Create New Post</router-link>
</div>
</template>
<script setup>
import PostCard from '../components/PostCard.vue';
// Data fetching logic will go here later (with Apollo)
const posts = []; // Placeholder
</script>
<style scoped>
/* Add basic styling */
.home { max-width: 800px; margin: 0 auto; padding: 20px; }
.post-list { margin: 20px 0; }
.create-btn { display: inline-block; padding: 8px 16px; background: #42b983; color: white; text-decoration: none; border-radius: 4px; }
</style>
Create a reusable PostCard component (src/components/PostCard.vue):
<!-- src/components/PostCard.vue -->
<template>
<div class="post-card">
<h2>{{ post.title }}</h2>
<p class="date">{{ new Date(post.createdAt).toLocaleDateString() }}</p>
<p class="excerpt">{{ post.content.substring(0, 100) }}...</p>
<router-link :to="`/posts/${post.id}`" class="read-more">Read More</router-link>
</div>
</template>
<script setup>
const props = defineProps({
post: { type: Object, required: true },
});
</script>
<style scoped>
.post-card { border: 1px solid #ddd; padding: 16px; margin-bottom: 16px; border-radius: 4px; }
.excerpt { color: #666; }
.read-more { color: #42b983; text-decoration: none; }
</style>
5.2 Post Detail Page
Create src/views/PostDetail.vue to display a single post:
<!-- src/views/PostDetail.vue -->
<template>
<div class="post-detail">
<h1>{{ post.title }}</h1>
<p class="date">{{ new Date(post.createdAt).toLocaleDateString() }}</p>
<div class="content">{{ post.content }}</div>
<router-link to="/" class="back-btn">Back to Home</router-link>
</div>
</template>
<script setup>
import { useRoute } from 'vue-router';
// Data fetching logic will go here later
const route = useRoute();
const post = { id: route.params.id, title: 'Loading...', content: 'Loading...' }; // Placeholder
</script>
5.3 Create Post Page
Create src/views/CreatePost.vue with a form to submit new posts:
<!-- src/views/CreatePost.vue -->
<template>
<div class="create-post">
<h1>Create New Post</h1>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label>Title:</label>
<input v-model="title" type="text" required />
</div>
<div class="form-group">
<label>Content:</label>
<textarea v-model="content" required></textarea>
</div>
<button type="submit">Create Post</button>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const title = ref('');
const content = ref('');
const router = useRouter();
const handleSubmit = async () => {
// Mutation logic will go here later (with Apollo)
alert('Post created!');
router.push('/');
};
</script>
Integrating Apollo Client with Vue
6.1 Setting Up Apollo Client
Create an src/apollo/index.js file to configure Apollo Client:
// src/apollo/index.js
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client/core';
import { setContext } from '@apollo/client/link/context';
// HTTP connection to the API
const httpLink = createHttpLink({
uri: 'http://localhost:4000', // GraphQL backend URL
});
// Create the Apollo Client instance
const apolloClient = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
});
export default apolloClient;
Update src/main.js to provide Apollo Client to the Vue app:
// src/main.js
import { createApp } from 'vue';
import { ApolloProvider } from '@apollo/client/vue3';
import App from './App.vue';
import router from './router';
import apolloClient from './apollo';
createApp(App)
.use(router)
.provide(ApolloProvider, apolloClient)
.mount('#app');
6.2 Fetching Data with useQuery
Update the Home Page to fetch posts using useQuery. First, define a GraphQL query in src/graphql/queries.js:
// src/graphql/queries.js
import { gql } from '@apollo/client/core';
export const GET_POSTS = gql`
query GetPosts {
posts {
id
title
content
createdAt
}
}
`;
export const GET_POST = gql`
query GetPost($id: ID!) {
post(id: $id) {
id
title
content
createdAt
}
}
`;
Now, update Home.vue to use useQuery:
<!-- src/views/Home.vue -->
<script setup>
import { useQuery } from '@apollo/client/vue3';
import { GET_POSTS } from '../graphql/queries';
import PostCard from '../components/PostCard.vue';
const { loading, error, data } = useQuery(GET_POSTS);
// Handle loading/error states
if (loading) console.log('Loading posts...');
if (error) console.error('Error fetching posts:', error);
const posts = data?.posts || [];
</script>
Similarly, update PostDetail.vue to fetch a single post by ID:
<!-- src/views/PostDetail.vue -->
<script setup>
import { useQuery } from '@apollo/client/vue3';
import { GET_POST } from '../graphql/queries';
import { useRoute } from 'vue-router';
const route = useRoute();
const { loading, error, data } = useQuery(GET_POST, {
variables: { id: route.params.id },
});
const post = loading ? { title: 'Loading...', content: 'Loading...' } : data?.post;
</script>
6.3 Mutating Data with useMutation
Define a mutation in src/graphql/mutations.js:
// src/graphql/mutations.js
import { gql } from '@apollo/client/core';
export const CREATE_POST = gql`
mutation CreatePost($title: String!, $content: String!) {
createPost(title: $title, content: $content) {
id
title
content
createdAt
}
}
`;
Update CreatePost.vue to use useMutation:
<!-- src/views/CreatePost.vue -->
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useMutation } from '@apollo/client/vue3';
import { CREATE_POST } from '../graphql/mutations';
const title = ref('');
const content = ref('');
const router = useRouter();
const [createPost, { loading }] = useMutation(CREATE_POST);
const handleSubmit = async () => {
try {
await createPost({ variables: { title: title.value, content: content.value } });
router.push('/');
} catch (error) {
console.error('Error creating post:', error);
}
};
</script>
Handling Authentication (Optional)
To add user authentication, extend the GraphQL schema with a User type and login mutation. Use JWT to generate tokens, store them in localStorage, and add an auth header to Apollo requests with setContext:
// src/apollo/index.js (updated)
const authLink = setContext((_, { headers }) => {
const token = localStorage.getItem('token');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
const apolloClient = new ApolloClient({
link: authLink.concat(httpLink), // Add auth link
cache: new InMemoryCache(),
});
Deployment
Frontend Deployment
Deploy the Vue app to Netlify or Vercel:
- Build the app:
npm run build - Upload the
distfolder to your hosting platform.
Backend Deployment
Deploy the GraphQL server to Railway or Heroku:
- Push the backend code to GitHub.
- Connect your repo to the hosting platform and set environment variables (e.g.,
DATABASE_URL).
Conclusion
You’ve built a fully functional blog with Vue.js and GraphQL! We covered setting up a GraphQL backend with Apollo Server and Prisma, creating Vue components with Vue Router, and integrating Apollo Client for data fetching. You can extend this further by adding comments, user profiles, or image uploads.