Zero-Bundle Analytics: Integrating CounterAPI with Qwik in 2025
Learn how to implement efficient analytics in Qwik applications using CounterAPI without compromising Qwik's zero-bundle-size philosophy.
Tutorial Expert
• 4 min read
Why CounterAPI Is Perfect for Qwik Applications
Qwik has revolutionized web development with its resumable architecture and near-zero JavaScript delivery. When adding analytics to a Qwik application, you need a solution that maintains these performance benefits while providing actionable insights.
CounterAPI offers an ideal match for Qwik's philosophy: lightweight, efficient, and focused on precisely what you need without unnecessary bloat.
In this guide, we'll explore how to integrate CounterAPI with Qwik for both client and server-side analytics.
Setting Up CounterAPI in Your Qwik Project
First, install the CounterAPI package:
npm install counterapi@latest
Server-Side Integration with Qwik City
Qwik City's server-side rendering capabilities make it perfect for tracking page views without impacting client-side performance.
Creating a CounterAPI Service
Let's create a utility file to manage our counter instances:
// src/utils/counter.ts
import { Counter } from 'counterapi';
import { server$ } from '@builder.io/qwik-city';
// For server-side operations
export const serverCounter = server$(async () => {
return new Counter({
workspace: process.env.COUNTER_WORKSPACE || '',
accessToken: process.env.COUNTER_TOKEN || '',
});
});
// For client-side use when needed
export function createClientCounter() {
return new Counter({
workspace: import.meta.env.PUBLIC_COUNTER_WORKSPACE,
accessToken: import.meta.env.PUBLIC_COUNTER_TOKEN,
});
}
// Server-side tracking utility
export const trackEvent = server$(async function(eventName: string) {
try {
const counter = await serverCounter();
const result = await counter.up(eventName);
return result.value;
} catch (error) {
console.error('Analytics error:', error);
return null;
}
});
Configure your environment variables in .env
:
COUNTER_WORKSPACE=your-workspace
COUNTER_TOKEN=server-token
PUBLIC_COUNTER_WORKSPACE=your-workspace
PUBLIC_COUNTER_TOKEN=public-token
Page View Tracking with Qwik Routes
Use Qwik's route loaders to track page views server-side:
// src/routes/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import { serverCounter } from '~/utils/counter';
export const usePageViews = routeLoader$(async () => {
try {
const counter = await serverCounter();
const result = await counter.up('home-page-view');
return result.value;
} catch (error) {
console.error('Failed to track page view:', error);
return 'unavailable';
}
});
export default component$(() => {
const pageViews = usePageViews();
return (
<div class="home-page">
<h1>Welcome to My Qwik Site!</h1>
<p>This page has been viewed {pageViews.value} times.</p>
{/* Rest of your page content */}
</div>
);
});
Tracking User Interactions
For client-side interactions, Qwik's unique approach requires thoughtful implementation:
Creating a Reusable Counter Component
// src/components/counter-button/counter-button.tsx
import { component$, useSignal, useVisibleTask$, useTask$ } from '@builder.io/qwik';
import { trackEvent } from '~/utils/counter';
interface CounterButtonProps {
eventName: string;
showCount?: boolean;
label: string;
}
export default component$((props: CounterButtonProps) => {
const count = useSignal<number | null>(null);
const loading = useSignal(true);
const clicked = useSignal(false);
// Load initial count if needed
useVisibleTask$(async ({ track }) => {
track(() => props.eventName);
if (props.showCount) {
try {
const value = await trackEvent(`${props.eventName}-get`);
count.value = value;
} catch (error) {
console.error('Error fetching count:', error);
} finally {
loading.value = false;
}
} else {
loading.value = false;
}
});
return (
<button
onClick$={async () => {
if (!clicked.value) {
clicked.value = true;
const newCount = await trackEvent(props.eventName);
if (props.showCount && newCount !== null) {
count.value = newCount;
}
}
}}
class={{
'counter-button': true,
'clicked': clicked.value
}}
>
{props.label}
{props.showCount && !loading.value &&
<span class="count">({count.value ?? 0})</span>
}
</button>
);
});
Using the Counter Button
// src/routes/features/index.tsx
import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import { serverCounter } from '~/utils/counter';
import CounterButton from '~/components/counter-button/counter-button';
// Track page view
export const usePageView = routeLoader$(async () => {
try {
const counter = await serverCounter();
await counter.up('features-page-view');
} catch (error) {
console.error('Analytics error:', error);
}
return true;
});
export default component$(() => {
usePageView();
const features = [
{ id: 1, name: 'Instant Loading', description: 'Zero delay with resumability' },
{ id: 2, name: 'Progressive Hydration', description: 'Load only what\'s needed' },
{ id: 3, name: 'Optimal Bundle Size', description: 'Minimal JavaScript shipped' }
];
return (
<div class="features-page">
<h1>Features</h1>
<div class="features-grid">
{features.map((feature) => (
<div key={feature.id} class="feature-card">
<h3>{feature.name}</h3>
<p>{feature.description}</p>
<CounterButton
eventName={`feature-${feature.id}-interest`}
label="I'm interested"
showCount={true}
/>
</div>
))}
</div>
</div>
);
});
Form Submission Analytics
Track form submissions with CounterAPI and Qwik's form actions:
// src/routes/contact/index.tsx
import { component$, useSignal } from '@builder.io/qwik';
import { routeAction$, zod$, z, Form } from '@builder.io/qwik-city';
import { serverCounter } from '~/utils/counter';
export const useContactAction = routeAction$(
async (data) => {
try {
// Process form data here
// Track successful submission
const counter = await serverCounter();
await counter.up('contact-form-success');
return {
success: true,
message: 'Thank you for your message!'
};
} catch (error) {
console.error('Form submission error:', error);
// Track error
try {
const counter = await serverCounter();
await counter.up('contact-form-error');
} catch (analyticsError) {
console.error('Analytics error:', analyticsError);
}
return {
success: false,
message: 'An error occurred. Please try again.'
};
}
},
zod$({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Valid email is required'),
message: z.string().min(10, 'Message must be at least 10 characters')
})
);
export default component$(() => {
const action = useContactAction();
return (
<div class="contact-page">
<h1>Contact Us</h1>
{action.value?.success ? (
<div class="success-message">
<h3>Message Sent!</h3>
<p>{action.value.message}</p>
</div>
) : (
<Form action={action} class="contact-form">
<div class="form-group">
<label for="name">Name</label>
<input
id="name"
name="name"
type="text"
class={{ error: !!action.value?.fieldErrors?.name }}
/>
{action.value?.fieldErrors?.name && (
<p class="error-message">{action.value.fieldErrors.name}</p>
)}
</div>
<div class="form-group">
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
class={{ error: !!action.value?.fieldErrors?.email }}
/>
{action.value?.fieldErrors?.email && (
<p class="error-message">{action.value.fieldErrors.email}</p>
)}
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea
id="message"
name="message"
rows={5}
class={{ error: !!action.value?.fieldErrors?.message }}
></textarea>
{action.value?.fieldErrors?.message && (
<p class="error-message">{action.value.fieldErrors.message}</p>
)}
</div>
<button type="submit">Send Message</button>
{action.value?.message && !action.value.success && (
<div class="error-banner">{action.value.message}</div>
)}
</Form>
)}
</div>
);
});
Global Navigation Tracking with Qwik Layout
Track all navigation events by adding a global tracker in your layout:
// src/routes/layout.tsx
import { component$, Slot, useVisibleTask$, useSignal } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city';
import { trackEvent } from '~/utils/counter';
export default component$(() => {
const location = useLocation();
const previousPath = useSignal<string | null>(null);
// Track page navigation
useVisibleTask$(({ track }) => {
track(() => location.url.pathname);
const currentPath = location.url.pathname;
// Don't track the initial page load (that's handled by route loaders)
// Only track navigation changes
if (previousPath.value !== null) {
const formattedPath = currentPath === '/'
? 'home'
: currentPath.substring(1).replace(/\//g, '-');
trackEvent(`navigate-to-${formattedPath}`);
}
previousPath.value = currentPath;
});
return (
<div class="app-container">
<header>
<nav>
{/* Your navigation */}
</nav>
</header>
<main>
<Slot />
</main>
<footer>
{/* Your footer */}
</footer>
</div>
);
});
Creating an Analytics Dashboard
Build a simple dashboard to view your analytics data:
// src/routes/admin/analytics/index.tsx
import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import { serverCounter } from '~/utils/counter';
// Fetch initial metrics on the server
export const useAnalyticsData = routeLoader$(async () => {
// In a real app, add authentication check here
const metricKeys = [
'home-page-view',
'features-page-view',
'contact-form-success',
'contact-form-error',
'feature-1-interest',
'feature-2-interest',
'feature-3-interest'
];
try {
const counter = await serverCounter();
const metrics = {};
for (const key of metricKeys) {
const result = await counter.get(key);
metrics[key] = result.value;
}
return { metrics, metricKeys };
} catch (error) {
console.error('Failed to fetch analytics:', error);
return { metrics: {}, metricKeys };
}
});
export default component$(() => {
const analyticsData = useAnalyticsData();
const metrics = useSignal(analyticsData.value.metrics);
const refreshing = useSignal(false);
return (
<div class="analytics-dashboard">
<header>
<h1>Analytics Dashboard</h1>
<button
onClick$={async () => {
refreshing.value = true;
try {
const counter = await serverCounter();
const updatedMetrics = {};
for (const key of analyticsData.value.metricKeys) {
const result = await counter.get(key);
updatedMetrics[key] = result.value;
}
metrics.value = updatedMetrics;
} catch (error) {
console.error('Failed to refresh metrics:', error);
} finally {
refreshing.value = false;
}
}}
disabled={refreshing.value}
>
{refreshing.value ? 'Refreshing...' : 'Refresh Metrics'}
</button>
</header>
<div class="metrics-grid">
{Object.entries(metrics.value).map(([key, value]) => (
<div key={key} class="metric-card">
<h3>{key.replace(/-/g, ' ')}</h3>
<p class="metric-value">{value}</p>
</div>
))}
</div>
</div>
);
});
Advanced: Creating an Endpoint for Client-Side Data
For authenticated clients that need to fetch analytics:
// src/routes/api/analytics/index.ts
import { serverCounter } from '~/utils/counter';
import type { RequestHandler } from '@builder.io/qwik-city';
export const onGet: RequestHandler = async ({ json, query, env }) => {
// In a real app, implement authentication here
const apiKey = query.get('apiKey');
if (apiKey !== env.get('ANALYTICS_API_KEY')) {
return json({ error: 'Unauthorized' }, 401);
}
const counterNames = query.get('counters')?.split(',') || [];
if (counterNames.length === 0) {
return json({ error: 'No counters specified' }, 400);
}
try {
const counter = await serverCounter();
const results = {};
for (const name of counterNames) {
const result = await counter.get(name);
results[name] = result.value;
}
return json({ results });
} catch (error) {
console.error('Analytics API error:', error);
return json({ error: 'Failed to fetch analytics' }, 500);
}
};
Conclusion
By integrating CounterAPI with Qwik, you get the best of both worlds: Qwik's revolutionary resumability and performance paired with CounterAPI's efficient analytics.
This approach maintains Qwik's zero-bundle-size philosophy by:
- Performing most analytics operations server-side
- Using Qwik's built-in server$ function for efficient client-server communication
- Minimizing client-side tracking code with the optional component pattern
- Leveraging Qwik City's routing and form handling for built-in analytics hooks
The result is a powerful analytics solution that doesn't compromise Qwik's performance benefits.
For more information about CounterAPI and its features, check out the official documentation.