javascriptroom guide

Building a Blog with Vue.js and GraphQL

In the world of web development, creating a blog often involves balancing frontend interactivity with efficient data management. Two tools that excel in these areas are **Vue.js** and **GraphQL**. Vue.js, a progressive JavaScript framework, simplifies building reactive user interfaces with its intuitive syntax and component-based architecture. GraphQL, on the other hand, revolutionizes data fetching by allowing clients to request exactly what they need, eliminating over-fetching and reducing network requests. In this tutorial, we’ll combine the power of Vue.js (frontend) and GraphQL (backend) to build a fully functional blog. You’ll learn how to set up a GraphQL server, create Vue components, integrate Apollo Client for data fetching, and deploy your application. By the end, you’ll have a responsive blog with features like listing posts, viewing single posts, and creating new posts.

Table of Contents

  1. Prerequisites
  2. Project Overview
  3. Setting Up the GraphQL Backend
  4. Setting Up the Vue.js Frontend
  5. Building Vue Components
  6. Integrating Apollo Client with Vue
  7. Handling Authentication (Optional)
  8. Deployment
  9. Conclusion
  10. 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:

  1. Build the app: npm run build
  2. Upload the dist folder to your hosting platform.

Backend Deployment

Deploy the GraphQL server to Railway or Heroku:

  1. Push the backend code to GitHub.
  2. 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.

References