Skip to main content
Vue.js for Beginners to Advanced
CHAPTER 19 Beginner

Working with REST APIs

Updated: May 18, 2026
5 min read

# CHAPTER 19

Working with REST APIs

1. Chapter Introduction

REST APIs are the backbone of modern web applications. In this chapter, we build a complete Blog management application — fetching, creating, updating, and deleting posts — demonstrating all four CRUD operations with real-world patterns like optimistic updates, pagination, and search.

2. Learning Objectives

  • Implement full CRUD (Create, Read, Update, Delete) with a REST API.
  • Handle optimistic UI updates.
  • Implement pagination and search.
  • Build reusable API service modules.
  • Create a blog management application.

3. REST API Service Layer

javascript
123456789101112131415161718192021222324252627282930313233343536
// src/api/posts.js
import axios from 'axios'

const BASE_URL = 'https://jsonplaceholder.typicode.com'

export const postsApi = {
  // READ — GET /posts
  getAll(params = {}) {
    return axios.get(`${BASE_URL}/posts`, { params })
  },

  // READ — GET /posts/:id
  getById(id) {
    return axios.get(`${BASE_URL}/posts/${id}`)
  },

  // CREATE — POST /posts
  create(postData) {
    return axios.post(`${BASE_URL}/posts`, postData)
  },

  // UPDATE — PUT /posts/:id (full replace)
  update(id, postData) {
    return axios.put(`${BASE_URL}/posts/${id}`, postData)
  },

  // PARTIAL UPDATE — PATCH /posts/:id
  patch(id, fields) {
    return axios.patch(`${BASE_URL}/posts/${id}`, fields)
  },

  // DELETE — DELETE /posts/:id
  delete(id) {
    return axios.delete(`${BASE_URL}/posts/${id}`)
  }
}

4. Mini Project: Blog API Application

vue
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
<!-- BlogApp.vue -->
<script setup>
import { ref, computed, onMounted } from &#039;vue'
import { postsApi } from &#039;@/api/posts'

// State
const posts = ref([])
const loading = ref(false)
const error = ref(null)
const searchQuery = ref(&#039;')
const currentPage = ref(1)
const pageSize = 6
const editingPost = ref(null)
const showForm = ref(false)

const form = ref({ title: &#039;', body: '', userId: 1 })

// Computed
const filteredPosts = computed(() =>
  posts.value.filter(p =>
    p.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
    p.body.toLowerCase().includes(searchQuery.value.toLowerCase())
  )
)

const paginatedPosts = computed(() => {
  const start = (currentPage.value - 1) * pageSize
  return filteredPosts.value.slice(start, start + pageSize)
})

const totalPages = computed(() =>
  Math.ceil(filteredPosts.value.length / pageSize)
)

// Fetch all posts
async function fetchPosts() {
  loading.value = true
  error.value = null
  try {
    const { data } = await postsApi.getAll()
    posts.value = data
  } catch (err) {
    error.value = &#039;Failed to load posts'
  } finally {
    loading.value = false
  }
}

// Create
async function createPost() {
  if (!form.value.title || !form.value.body) return
  try {
    const { data } = await postsApi.create({ ...form.value })
    posts.value.unshift({ ...data, id: Date.now() })  // Optimistic add
    resetForm()
  } catch (err) {
    error.value = &#039;Failed to create post'
  }
}

// Update
async function updatePost() {
  try {
    await postsApi.update(editingPost.value.id, form.value)
    const index = posts.value.findIndex(p => p.id === editingPost.value.id)
    if (index !== -1) posts.value[index] = { ...editingPost.value, ...form.value }
    resetForm()
  } catch (err) {
    error.value = &#039;Failed to update post'
  }
}

// Delete with optimistic UI
async function deletePost(id) {
  const backup = [...posts.value]
  posts.value = posts.value.filter(p => p.id !== id)  // Optimistic remove
  try {
    await postsApi.delete(id)
  } catch (err) {
    posts.value = backup  // Rollback on failure
    error.value = &#039;Failed to delete post'
  }
}

function editPost(post) {
  editingPost.value = post
  form.value = { title: post.title, body: post.body, userId: post.userId }
  showForm.value = true
}

function resetForm() {
  editingPost.value = null
  form.value = { title: &#039;', body: '', userId: 1 }
  showForm.value = false
}

function submitForm() {
  editingPost.value ? updatePost() : createPost()
}

onMounted(fetchPosts)
</script>

<template>
  <div class="blog-app">
    <!-- Header -->
    <header class="blog-header">
      <h1>📝 Blog Manager</h1>
      <div class="header-actions">
        <input v-model="searchQuery" placeholder="Search posts..." class="search-input" @input="currentPage = 1" />
        <button @click="showForm = !showForm" class="btn-primary">
          {{ showForm ? &#039;✕ Cancel' : '+ New Post' }}
        </button>
      </div>
    </header>

    <!-- Post Form -->
    <div v-if="showForm" class="post-form">
      <h2>{{ editingPost ? &#039;Edit Post' : 'New Post' }}</h2>
      <form @submit.prevent="submitForm">
        <input v-model="form.title" placeholder="Post title..." required />
        <textarea v-model="form.body" placeholder="Post body..." rows="4" required></textarea>
        <div class="form-actions">
          <button type="button" @click="resetForm" class="btn-secondary">Cancel</button>
          <button type="submit" class="btn-primary">{{ editingPost ? &#039;Update' : 'Publish' }}</button>
        </div>
      </form>
    </div>

    <!-- States -->
    <div v-if="loading" class="state-msg">⏳ Loading posts...</div>
    <div v-else-if="error" class="error-msg">{{ error }} <button @click="fetchPosts">Retry</button></div>

    <!-- Post Grid -->
    <div v-else class="posts-grid">
      <article v-for="post in paginatedPosts" :key="post.id" class="post-card">
        <div class="post-meta"># {{ post.id }}</div>
        <h3>{{ post.title }}</h3>
        <p>{{ post.body.substring(0, 120) }}...</p>
        <div class="post-actions">
          <button @click="editPost(post)" class="btn-edit">✏️ Edit</button>
          <button @click="deletePost(post.id)" class="btn-delete">🗑️ Delete</button>
        </div>
      </article>
    </div>

    <!-- Empty State -->
    <div v-if="!loading && filteredPosts.length === 0" class="empty">
      <p>No posts found matching "{{ searchQuery }}"</p>
      <button @click="searchQuery = &#039;'">Clear search</button>
    </div>

    <!-- Pagination -->
    <div v-if="totalPages > 1" class="pagination">
      <button @click="currentPage--" :disabled="currentPage === 1">← Prev</button>
      <span>Page {{ currentPage }} of {{ totalPages }}</span>
      <button @click="currentPage++" :disabled="currentPage === totalPages">Next →</button>
    </div>
  </div>
</template>

<style scoped>
.blog-app { max-width: 1100px; margin: 0 auto; padding: 2rem; font-family: sans-serif; }
.blog-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem; }
h1 { font-size: 1.75rem; color: #1e293b; margin: 0; }
.header-actions { display: flex; gap: .75rem; }
.search-input { padding: .6rem 1rem; border: 2px solid #e2e8f0; border-radius: 8px; outline: none; width: 220px; }
.search-input:focus { border-color: #6366f1; }
.btn-primary { background: #6366f1; color: white; border: none; padding: .6rem 1.25rem; border-radius: 8px; cursor: pointer; font-weight: 600; }
.btn-secondary { background: #e2e8f0; color: #475569; border: none; padding: .6rem 1.25rem; border-radius: 8px; cursor: pointer; }
.post-form { background: white; border: 2px solid #e2e8f0; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
.post-form input, .post-form textarea { width: 100%; padding: .75rem; border: 1px solid #e2e8f0; border-radius: 8px; font-size: .95rem; margin-bottom: .75rem; box-sizing: border-box; }
.form-actions { display: flex; gap: .75rem; justify-content: flex-end; }
.posts-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.25rem; }
.post-card { background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 1.25rem; transition: .15s; }
.post-card:hover { border-color: #6366f1; box-shadow: 0 4px 12px rgba(99,102,241,.1); }
.post-meta { color: #6366f1; font-size: .8rem; font-weight: 600; margin-bottom: .5rem; }
h3 { margin: 0 0 .5rem; font-size: 1rem; line-height: 1.4; }
p { color: #64748b; font-size: .875rem; line-height: 1.6; }
.post-actions { display: flex; gap: .5rem; margin-top: 1rem; }
.btn-edit { background: #f0fdf4; color: #16a34a; border: 1px solid #bbf7d0; padding: .35rem .75rem; border-radius: 6px; cursor: pointer; font-size: .85rem; }
.btn-delete { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; padding: .35rem .75rem; border-radius: 6px; cursor: pointer; font-size: .85rem; }
.pagination { display: flex; justify-content: center; align-items: center; gap: 1rem; margin-top: 2rem; }
.pagination button { padding: .5rem 1rem; border: 1px solid #e2e8f0; border-radius: 8px; cursor: pointer; background: white; }
.pagination button:disabled { opacity: .4; cursor: not-allowed; }
.empty { text-align: center; padding: 3rem; color: #64748b; }
.state-msg { text-align: center; padding: 3rem; color: #64748b; }
.error-msg { background: #fef2f2; color: #dc2626; padding: 1rem; border-radius: 8px; margin-bottom: 1rem; }
</style>

5. Common Mistakes

  • Not implementing optimistic UI: Waiting for the server to confirm before updating the UI makes the app feel slow. Optimistically update the UI and rollback on failure.
  • Hardcoding the API base URL: Use environment variables (VITEAPIURL) for flexibility across dev/staging/production.

6. MCQs

Question 1

CRUD stands for?

Question 2

Optimistic UI update means?

Question 3

REST DELETE request returns?

Question 4

Pagination: which posts to show on page 2 of 6 per page?

Question 5

array.findIndex() is used for?

Question 6

Why use a service layer (postsApi.js)?

Question 7

unshift() on an array?

Question 8

Rollback in optimistic UI?

Question 9

Query params in Axios GET?

Question 10

REST convention for listing resources?

7. Interview Questions

  • Q: Explain optimistic UI updates and how you would implement them in Vue.
  • Q: How would you build a paginated list in Vue that fetches data from an API?

8. Summary

Full CRUD with REST APIs follows the service layer pattern: dedicated API modules, component state management (loading/error/data), optimistic UI for instant feedback, and pagination for large datasets. This is the foundation of every data-driven Vue application.

9. Next Chapter Recommendation

In Chapter 20: Vue Composition API, we master the setup() function, composables, and advanced Composition API patterns that power modern Vue 3 development.

Finish this Chapter

Save your progress on your learning path and prepare for coding interview challenges.

Discussion

Join the discussion

Log in or create a free account to participate.

Sort: ·