Tutorial

Integrating CounterAPI with SvelteKit: A 2025 Guide

Learn how to leverage CounterAPI's lightweight analytics in your SvelteKit applications for efficient tracking and insightful metrics.

TH

Tutorial Expert

• 4 min read

CounterAPI SvelteKit Svelte Analytics Web Development

Why SvelteKit and CounterAPI Work So Well Together

SvelteKit has maintained its reputation as one of the most elegant and efficient frameworks for building web applications. Its lightweight approach to reactivity pairs perfectly with CounterAPI's minimalist analytics solution.

In this guide, we'll explore how to integrate CounterAPI into your SvelteKit projects to track metrics efficiently in both server and client environments.

Setting Up CounterAPI in SvelteKit

First, install the CounterAPI package:

npm install counterapi@latest

Creating a CounterAPI Store

Let's leverage Svelte's stores to create a reusable CounterAPI client:

// src/lib/counter.js
import { Counter } from 'counterapi';
import { browser } from '$app/environment';
import { env } from '$env/dynamic/public';
import { writable } from 'svelte/store';

// For client-side use
export const createClientCounter = () => {
  if (!browser) return null;
  
  return new Counter({
    workspace: env.PUBLIC_COUNTER_WORKSPACE,
    accessToken: env.PUBLIC_COUNTER_TOKEN
  });
};

// For use in components
export const counterStore = writable(null);

// Initialize the store when in browser
if (browser) {
  counterStore.set(createClientCounter());
}

// For server-side use (Server API routes and server load functions)
import { COUNTER_WORKSPACE, COUNTER_TOKEN } from '$env/static/private';

let serverCounter;
export const getServerCounter = () => {
  if (!serverCounter) {
    serverCounter = new Counter({
      workspace: COUNTER_WORKSPACE,
      accessToken: COUNTER_TOKEN
    });
  }
  return serverCounter;
};

Create a .env file to store your environment variables:

PUBLIC_COUNTER_WORKSPACE=your-workspace
PUBLIC_COUNTER_TOKEN=public-token
COUNTER_WORKSPACE=your-workspace
COUNTER_TOKEN=private-token

Page View Tracking with SvelteKit

Server-Side Tracking in Load Functions

SvelteKit's load functions are perfect for tracking page views:

// src/routes/+page.server.js
import { getServerCounter } from '$lib/counter';

/** @type {import('./$types').PageServerLoad} */
export async function load() {
  const counter = getServerCounter();
  let pageViews;
  
  try {
    const result = await counter.up('home-page-view');
    pageViews = result.value;
  } catch (error) {
    console.error('Analytics error:', error);
    pageViews = 'unavailable';
  }
  
  return {
    pageViews
  };
}

Using the Data in Your Page

<!-- src/routes/+page.svelte -->
<script>
  /** @type {import('./$types').PageData} */
  export let data;
</script>

<h1>Welcome to my SvelteKit site</h1>
<p>This page has been viewed {data.pageViews} times.</p>

<!-- Rest of your page content -->

Client-Side Interaction Tracking

Create a simple counter component for tracking user interactions:

<!-- src/lib/components/InteractionCounter.svelte -->
<script>
  import { onMount } from 'svelte';
  import { counterStore } from '$lib/counter';
  
  export let event = 'button-click'; // The event to track
  export let showCount = false; // Whether to display the count
  
  let count = 0;
  let loading = true;
  
  // Load initial count if showing it
  onMount(async () => {
    if (showCount && $counterStore) {
      try {
        const result = await $counterStore.get(event);
        count = result.value;
      } catch (error) {
        console.error(`Error fetching ${event} count:`, error);
      } finally {
        loading = false;
      }
    } else {
      loading = false;
    }
  });
  
  // Track the interaction
  async function trackInteraction() {
    if (!$counterStore) return;
    
    try {
      const result = await $counterStore.up(event);
      if (showCount) {
        count = result.value;
      }
    } catch (error) {
      console.error(`Error tracking ${event}:`, error);
    }
  }
</script>

<slot {trackInteraction} {count} {loading} />

Using the Interaction Counter

<!-- src/routes/features/+page.svelte -->
<script>
  import InteractionCounter from '$lib/components/InteractionCounter.svelte';
  
  const features = [
    { id: 1, name: 'Automated Reports', description: 'Generate reports automatically based on your data.' },
    { id: 2, name: 'Data Visualization', description: 'See your data come to life with interactive charts.' },
    { id: 3, name: 'API Integration', description: 'Connect with other services via our robust API.' }
  ];
</script>

<h1>Features</h1>

<div class="features-grid">
  {#each features as feature}
    <div class="feature-card">
      <h3>{feature.name}</h3>
      <p>{feature.description}</p>
      
      <InteractionCounter event={`feature-${feature.id}-view-details`} showCount={true}>
        {#key count}
          <button on:click={trackInteraction}>
            View Details ({count} views)
          </button>
        {/key}
      </InteractionCounter>
    </div>
  {/each}
</div>

<style>
  .features-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 2rem;
  }
  
  .feature-card {
    padding: 1.5rem;
    border-radius: 8px;
    background: #f9f9f9;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  }
</style>

Auto-Tracking Page Views with Layout

Track all page navigations automatically using SvelleKit's layout:

<!-- src/routes/+layout.svelte -->
<script>
  import { page } from '$app/stores';
  import { counterStore } from '$lib/counter';
  import { onMount } from 'svelte';
  import { afterNavigate } from '$app/navigation';
  
  // Track initial page view
  onMount(() => {
    trackPageView($page.url.pathname);
  });
  
  // Track subsequent navigations
  afterNavigate(({ to }) => {
    if (to?.url) {
      trackPageView(to.url.pathname);
    }
  });
  
  function trackPageView(path) {
    if (!$counterStore) return;
    
    // Format the path for counter name
    const formattedPath = path === '/' ? 'home' : path.substring(1).replace(/\//g, '-');
    
    $counterStore.up(`page-${formattedPath}`)
      .catch(error => console.error('Page tracking error:', error));
  }
</script>

<!-- SvelteKit's default slot -->
<slot />

API Endpoints for Analytics Data

Create a server API endpoint to fetch analytics data:

// src/routes/api/analytics/+server.js
import { getServerCounter } from '$lib/counter';
import { json } from '@sveltejs/kit';

/** @type {import('./$types').RequestHandler} */
export async function GET({ url }) {
  // In a real app, add authentication here
  
  const counterNames = url.searchParams.get('counters')?.split(',') || [];
  
  if (counterNames.length === 0) {
    return json({ error: 'No counters specified' }, { status: 400 });
  }
  
  try {
    const counter = getServerCounter();
    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' }, { status: 500 });
  }
}

Building an Analytics Dashboard

Create a simple dashboard to visualize your metrics:

<!-- src/routes/admin/analytics/+page.server.js -->
import { getServerCounter } from '$lib/counter';
import { redirect } from '@sveltejs/kit';

/** @type {import('./$types').PageServerLoad} */
export async function load({ locals }) {
  // In a real app, check authentication here
  // if (!locals.user?.isAdmin) throw redirect(302, '/login');
  
  const metricKeys = [
    'home-page-view',
    'blog-page-view',
    'feature-1-view-details',
    'feature-2-view-details',
    'feature-3-view-details',
    'signup-button-click',
    'contact-form-submit'
  ];
  
  const counter = getServerCounter();
  const metrics = {};
  
  // Fetch all metrics
  try {
    for (const key of metricKeys) {
      const result = await counter.get(key);
      metrics[key] = result.value;
    }
  } catch (error) {
    console.error('Error fetching metrics:', error);
  }
  
  return { metrics };
}
<!-- src/routes/admin/analytics/+page.svelte -->
<script>
  /** @type {import('./$types').PageData} */
  export let data;
  
  import { onMount } from 'svelte';
  
  let metrics = data.metrics;
  let loading = false;
  
  async function refreshMetrics() {
    loading = true;
    
    try {
      const keys = Object.keys(metrics).join(',');
      const response = await fetch(`/api/analytics?counters=${keys}`);
      
      if (!response.ok) throw new Error('Failed to refresh metrics');
      
      const data = await response.json();
      metrics = data.results;
    } catch (error) {
      console.error('Error refreshing metrics:', error);
      alert('Failed to refresh metrics');
    } finally {
      loading = false;
    }
  }
</script>

<div class="dashboard">
  <header>
    <h1>Analytics Dashboard</h1>
    <button on:click={refreshMetrics} disabled={loading}>
      {loading ? 'Refreshing...' : 'Refresh Metrics'}
    </button>
  </header>
  
  <div class="metrics-grid">
    {#each Object.entries(metrics) as [key, value]}
      <div class="metric-card">
        <h3>{key.replace(/-/g, ' ')}</h3>
        <p class="value">{value}</p>
      </div>
    {/each}
  </div>
</div>

<style>
  .dashboard {
    padding: 1.5rem;
  }
  
  header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 2rem;
  }
  
  .metrics-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
    gap: 1.5rem;
  }
  
  .metric-card {
    background: #f5f5f5;
    border-radius: 8px;
    padding: 1.5rem;
    box-shadow: 0 2px 4px rgba(0,0,0,0.05);
  }
  
  .value {
    font-size: 2rem;
    font-weight: bold;
    margin: 0.5rem 0 0 0;
  }
</style>

Form Submission Tracking

Track form submissions with SvelteKit's form actions:

<!-- src/routes/contact/+page.server.js -->
import { getServerCounter } from '$lib/counter';
import { fail } from '@sveltejs/kit';

/** @type {import('./$types').Actions} */
export const actions = {
  default: async ({ request }) => {
    const formData = await request.formData();
    const name = formData.get('name');
    const email = formData.get('email');
    const message = formData.get('message');
    
    // Validate form data
    const errors = {};
    if (!name) errors.name = 'Name is required';
    if (!email) errors.email = 'Email is required';
    if (!message) errors.message = 'Message is required';
    
    if (Object.keys(errors).length > 0) {
      // Track form validation error
      try {
        const counter = getServerCounter();
        await counter.up('contact-form-error');
      } catch (error) {
        console.error('Analytics error:', error);
      }
      
      return fail(400, { errors, name, email, message });
    }
    
    try {
      // Process form submission here
      
      // Track successful form submission
      const counter = getServerCounter();
      await counter.up('contact-form-success');
      
      return {
        success: true
      };
    } catch (error) {
      console.error('Form submission error:', error);
      
      // Track form processing error
      try {
        const counter = getServerCounter();
        await counter.up('contact-form-server-error');
      } catch (analyticsError) {
        console.error('Analytics error:', analyticsError);
      }
      
      return fail(500, {
        message: 'An error occurred while submitting the form',
        name, email, message
      });
    }
  }
};
<!-- src/routes/contact/+page.svelte -->
<script>
  /** @type {import('./$types').ActionData} */
  export let form;
  
  let name = '';
  let email = '';
  let message = '';
  
  // Restore form values if submission failed
  $: {
    if (form && !form.success) {
      name = form.name || '';
      email = form.email || '';
      message = form.message || '';
    }
  }
</script>

<h1>Contact Us</h1>

{#if form?.success}
  <div class="success-message">
    <h2>Thank you for your message!</h2>
    <p>We'll get back to you as soon as possible.</p>
  </div>
{:else}
  <form method="POST">
    <div class="form-group">
      <label for="name">Name</label>
      <input 
        type="text" 
        id="name" 
        name="name" 
        value={name} 
        class:error={form?.errors?.name}
      />
      {#if form?.errors?.name}
        <p class="error-message">{form.errors.name}</p>
      {/if}
    </div>
    
    <div class="form-group">
      <label for="email">Email</label>
      <input 
        type="email" 
        id="email" 
        name="email" 
        value={email}
        class:error={form?.errors?.email}
      />
      {#if form?.errors?.email}
        <p class="error-message">{form.errors.email}</p>
      {/if}
    </div>
    
    <div class="form-group">
      <label for="message">Message</label>
      <textarea 
        id="message" 
        name="message" 
        rows="5"
        class:error={form?.errors?.message}
      >{message}</textarea>
      {#if form?.errors?.message}
        <p class="error-message">{form.errors.message}</p>
      {/if}
    </div>
    
    <button type="submit">Send Message</button>
  </form>
{/if}

<style>
  .form-group {
    margin-bottom: 1.5rem;
  }
  
  label {
    display: block;
    margin-bottom: 0.5rem;
  }
  
  input, textarea {
    width: 100%;
    padding: 0.75rem;
    border: 1px solid #ddd;
    border-radius: 4px;
  }
  
  .error {
    border-color: red;
  }
  
  .error-message {
    color: red;
    font-size: 0.875rem;
    margin: 0.25rem 0 0 0;
  }
  
  .success-message {
    background: #e6f7e6;
    border-left: 4px solid #2e7d32;
    padding: 1rem 1.5rem;
    border-radius: 4px;
  }
</style>

Conclusion

By integrating CounterAPI with SvelteKit, you can implement a lightweight, efficient analytics solution that leverages SvelteKit's reactivity and server capabilities. This combination allows you to track important metrics without compromising performance or user experience.

The Svelte store-based approach makes it easy to use CounterAPI throughout your application, whether you need to track page views, form submissions, or user interactions.

For more information about CounterAPI and additional features, visit the official documentation.