Most football apps eventually need one search box that finds both teams and players โ think of the search bar at the top of any major sports app. This guide builds that experience: debounced input, combined results, and keyboard-friendly result navigation.
Why Combine Two Endpoints Into One Search Box
Live Football API has separate /team_search and /player_search endpoints โ there's no single combined search endpoint. A universal search bar means firing both in parallel and merging the results client-side.
Step 1: Fetching Both Result Types in Parallel
async function universalSearch(query) {
const [teamsRes, playersRes] = await Promise.all([
fetch(`https://live-football-api.com/api/v1/team_search?api_key=YOUR_KEY&q=${encodeURIComponent(query)}`),
fetch(`https://live-football-api.com/api/v1/player_search?api_key=YOUR_KEY&q=${encodeURIComponent(query)}`)
]);
const [teams, players] = await Promise.all([teamsRes.json(), playersRes.json()]);
return {
teams: teams.data.teams.map(t => ({ ...t, type: 'team' })),
players: players.data.players.map(p => ({ ...p, type: 'player' }))
};
}
Tagging each result with a type field up front makes rendering and routing much simpler downstream.
Step 2: Debouncing Input
Firing two API calls on every keystroke wastes credits fast. Debounce so a call only fires once the user pauses typing:
function debounce(fn, delay = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const debouncedSearch = debounce(async (query) => {
if (query.length < 2) {
clearResults();
return;
}
const results = await universalSearch(query);
renderResults(results);
}, 300);
searchInput.addEventListener('input', (e) => debouncedSearch(e.target.value));
Step 3: Rendering Grouped Results
Group teams and players into separate labeled sections rather than one flat list โ this is the pattern most users already expect from search UIs:
function renderResults({ teams, players }) {
const html = `
${teams.length > 0 ? `
<div class="search-group">
<h4>Teams</h4>
${teams.map(t => `
<div class="result-item" data-type="team" data-id="${t.id}">
<img src="${t.logo}" alt="" />
<span>${t.name}</span>
</div>
`).join('')}
</div>
` : ''}
${players.length > 0 ? `
<div class="search-group">
<h4>Players</h4>
${players.map(p => `
<div class="result-item" data-type="player" data-id="${p.id}">
<img src="${p.photo}" alt="" />
<span>${p.name}</span>
</div>
`).join('')}
</div>
` : ''}
`;
resultsContainer.innerHTML = html || '<p class="no-results">No results found</p>';
}
Step 4: Handling Result Selection
Route based on the data-type attribute set during rendering โ teams and players lead to different detail pages:
resultsContainer.addEventListener('click', (e) => {
const item = e.target.closest('.result-item');
if (!item) return;
const { type, id } = item.dataset;
if (type === 'team') {
navigateTo(`/team/${id}`);
} else {
navigateTo(`/player/${id}`);
}
});
Step 5: Adding Keyboard Navigation
A search bar isn't complete without arrow-key navigation and Enter-to-select โ a small addition that makes the feature feel production-ready:
let activeIndex = -1;
searchInput.addEventListener('keydown', (e) => {
const items = resultsContainer.querySelectorAll('.result-item');
if (!items.length) return;
if (e.key === 'ArrowDown') {
activeIndex = Math.min(activeIndex + 1, items.length - 1);
} else if (e.key === 'ArrowUp') {
activeIndex = Math.max(activeIndex - 1, 0);
} else if (e.key === 'Enter' && activeIndex >= 0) {
items[activeIndex].click();
return;
} else {
return;
}
items.forEach((el, i) => el.classList.toggle('active', i === activeIndex));
});
Reducing Credit Usage on a High-Traffic Search Bar
- Debounce aggressively โ 300-400ms is a reasonable default; shorter feels snappier but costs more credits
- Set a minimum query length โ skip searching on 1-character inputs, which return too many results to be useful anyway
- Cache recent queries client-side for a session โ if a user retypes something they searched moments ago, serve from memory instead of re-calling the API
const searchCache = new Map();
async function cachedSearch(query) {
if (searchCache.has(query)) return searchCache.get(query);
const results = await universalSearch(query);
searchCache.set(query, results);
return results;
}
Frequently Asked Questions
Is there a single endpoint that searches both teams and players at once?
No, /team_search and /player_search are separate endpoints โ combining them into one UI requires calling both in parallel as shown above.
How many credits does one universal search use?
2 credits per completed search (1 for team_search, 1 for player_search), before any debouncing or caching optimizations.
Can I also include league results in the same search bar?
Yes, add a third parallel call to /league_search (available on some sibling APIs) or filter locally from a cached /leagues response if you only need league name matching.
What's a reasonable debounce delay for a search bar?
250-400ms is typical โ short enough to feel responsive, long enough to avoid firing a request on every single keystroke.