DEV Community

Joffy122
Joffy122

Posted on

How to Build a Real-Time Search Engine Interface in Vue.js 3 with Joffstrends Search API

How to Build a Real-Time Search Engine Interface in Vue.js 3 with Joffstrends Search API

In modern web development, providing users with a fast, responsive, and accurate search interface is a crucial feature. Whether you are building a news aggregator, a content discovery platform, or a custom dashboard, integrating a reliable search API can elevate your application's user experience.

In this tutorial, we will walk through how to build a real-time search interface using Vue.js 3 (with the Composition API and Vite) and the Joffstrends Search API.

The Joffstrends Search API is an incredibly affordable, high-performance search API designed for developers. With plans starting as low as £4.99 for a starter pack or £9.99/month for 1,000 searches, it is an excellent alternative to more expensive search APIs on the market. You can grab your API key from Gumroad and get started in minutes.


Prerequisites

Before we begin, make sure you have the following:

  1. Node.js (version 16 or higher) installed on your machine.
  2. Basic familiarity with Vue.js 3 and the Composition API.
  3. A Joffstrends Search API Key (which you can obtain from the Gumroad Monthly Plan or the Starter Plan).

Step 1: Setting Up Your Vue.js 3 Project

We will use Vite to scaffold a new Vue 3 project quickly. Open your terminal and run:

npm create vue@latest vue-joffstrends-search
Enter fullscreen mode Exit fullscreen mode

Follow the prompts to configure your project. For this tutorial, we can keep it simple:

  • Add TypeScript? No
  • Add JSX Support? No
  • Add Vue Router? No
  • Add Pinia? No
  • Add Vitest? No
  • Add ESLint? Yes

Once the scaffolding is complete, navigate into your project directory and install the dependencies:

cd vue-joffstrends-search
npm install
Enter fullscreen mode Exit fullscreen mode

Now, let's start the development server to make sure everything is working:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Step 2: Configuring Environment Variables

To keep our API key secure, we should store it in an environment variable. Create a .env file in the root of your project:

VITE_JOFFSTRENDS_API_KEY=your_actual_api_key_here
Enter fullscreen mode Exit fullscreen mode

Vite automatically loads environment variables prefixed with VITE_, making them accessible in our Vue components via import.meta.env.VITE_JOFFSTRENDS_API_KEY.


Step 3: Creating the Search Service

Let's create a clean, reusable service to handle our API requests. Create a new file named src/services/searchService.js:

const API_URL = 'https://api.joffstrends.co.uk/search';
const API_KEY = import.meta.env.VITE_JOFFSTRENDS_API_KEY;

export async function fetchSearchResults(query) {
  if (!query || query.trim() === '') {
    return [];
  }

  try {
    const response = await fetch(`${API_URL}?q=${encodeURIComponent(query)}`, {
      method: 'GET',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      }
    });

    if (!response.ok) {
      throw new Error(`API Error: ${response.status} ${response.statusText}`);
    }

    const data = await response.json();
    return data.results || data || [];
  } catch (error) {
    console.error('Failed to fetch search results:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Building the Search Component

Now, let's build a highly reactive search component. We will implement debouncing to prevent making API calls on every single keystroke, which helps save API quota and improves performance.

Create a new component at src/components/SearchInterface.vue:

<template>
  <div class="search-container">
    <h1>Joffstrends Search Explorer</h1>
    <p class="subtitle">Powering real-time search with Vue 3 and Joffstrends API</p>

    <div class="search-box">
      <input
        v-model="searchQuery"
        type="text"
        placeholder="Search for news, trends, or topics..."
        @input="handleInput"
        class="search-input"
      />
      <button @click="triggerSearch" class="search-button" :disabled="loading">
        Search
      </button>
    </div>

    <!-- Loading State -->
    <div v-if="loading" class="status-message loading">
      <div class="spinner"></div> Searching...
    </div>

    <!-- Error State -->
    <div v-if="error" class="status-message error">
      {{ error }}
    </div>

    <!-- Results List -->
    <div v-if="results.length > 0" class="results-list">
      <div v-for="(result, index) in results" :key="index" class="result-card">
        <h3>
          <a :href="result.url" target="_blank" rel="noopener noreferrer">
            {{ result.title || 'Untitled Result' }}
          </a>
        </h3>
        <p class="snippet">{{ result.snippet || 'No description available.' }}</p>
        <span class="source" v-if="result.source">{{ result.source }}</span>
      </div>
    </div>

    <!-- Empty State -->
    <div v-else-if="searchQuery && !loading && !error" class="status-message empty">
      No results found for "{{ searchQuery }}". Try another query!
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { fetchSearchResults } from '../services/searchService';

const searchQuery = ref('');
const results = ref([]);
const loading = ref(false);
const error = ref(null);
let debounceTimeout = null;

const triggerSearch = async () => {
  if (!searchQuery.value.trim()) {
    results.value = [];
    return;
  }

  loading.value = true;
  error.value = null;

  try {
    results.value = await fetchSearchResults(searchQuery.value);
  } catch (err) {
    error.value = 'An error occurred while fetching search results. Please try again.';
    results.value = [];
  } finally {
    loading.value = false;
  }
};

const handleInput = () => {
  if (debounceTimeout) {
    clearTimeout(debounceTimeout);
  }

  debounceTimeout = setTimeout(() => {
    triggerSearch();
  }, 500);
};
</script>

<style scoped>
.search-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem;
  font-family: 'Inter', sans-serif;
}

h1 {
  color: #2c3e50;
  margin-bottom: 0.5rem;
  text-align: center;
}

.subtitle {
  color: #7f8c8d;
  text-align: center;
  margin-bottom: 2rem;
}

.search-box {
  display: flex;
  gap: 10px;
  margin-bottom: 2rem;
}

.search-input {
  flex: 1;
  padding: 12px 20px;
  font-size: 16px;
  border: 2px solid #ddd;
  border-radius: 8px;
  outline: none;
  transition: border-color 0.3s;
}

.search-input:focus {
  border-color: #3498db;
}

.search-button {
  padding: 12px 24px;
  font-size: 16px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.search-button:hover {
  background-color: #2980b9;
}

.search-button:disabled {
  background-color: #bdc3c7;
  cursor: not-allowed;
}

.status-message {
  text-align: center;
  font-size: 16px;
  margin: 2rem 0;
}

.error {
  color: #e74c3c;
}

.results-list {
  display: flex;
  flex-direction: column;
  gap: 1.5rem;
}

.result-card {
  padding: 1.5rem;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
  transition: transform 0.2s, box-shadow 0.2s;
}

.result-card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}

.result-card h3 {
  margin: 0 0 0.5rem 0;
}

.result-card a {
  color: #2b6cb0;
  text-decoration: none;
}

.result-card a:hover {
  text-decoration: underline;
}

.snippet {
  color: #4a5568;
  font-size: 14px;
  line-height: 1.6;
  margin: 0 0 0.75rem 0;
}

.source {
  font-size: 12px;
  color: #718096;
  background-color: #edf2f7;
  padding: 4px 8px;
  border-radius: 4px;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Step 5: Integrating the Component in App.vue

Now, let's clean up src/App.vue and render our new SearchInterface component. Replace the contents of src/App.vue with:

<template>
  <main>
    <SearchInterface />
  </main>
</template>

<script setup>
import SearchInterface from './components/SearchInterface.vue';
</script>

<style>
body {
  background-color: #f7fafc;
  margin: 0;
  padding: 0;
}
</style>
Enter fullscreen mode Exit fullscreen mode

Why Choose Joffstrends Search API?

When building search-driven applications, developers are often faced with complex setups or expensive monthly fees. The Joffstrends Search API simplifies this by offering:

  1. Simplicity: A single, clean endpoint (https://api.joffstrends.co.uk/search?q=query) that returns structured JSON results.
  2. Affordability: Perfect for indie hackers, side projects, and startups. You don't need to commit to hundreds of dollars a month just to get reliable search results.
  3. Speed: Fast response times ensure your Vue.js reactive interfaces feel snappy and instant.

To get your API key, check out the options on Gumroad:


Conclusion

With Vue 3's Composition API and the Joffstrends Search API, building a real-time, debounced search interface is incredibly straightforward. You can easily extend this project by adding pagination, search filters, or saving search history in localStorage.

Happy coding! Let us know in the comments what you plan to build with Vue and Joffstrends.

Top comments (0)