HTTP Utilities

Svelar provides two HTTP layers:

  • Client-side (apiFetch) — CSRF-aware fetch for calling your own API from Svelte components
  • Server-side (Http) — Fluent HTTP client for calling third-party APIs (Postmark, Stripe, Mailchimp, etc.)

Server-Side HTTP Client

The Http client is designed for server-to-server communication — calling external APIs from your controllers, services, and jobs. It provides a fluent, immutable API inspired by Laravel's Http facade.

Import

import { Http } from '@beeblock/svelar/http';

Basic Usage

// GET request
const response = await Http
  .baseUrl('https://api.example.com')
  .withToken(process.env.API_TOKEN!)
  .get('/users');

console.log(response.data);   // parsed JSON
console.log(response.status); // 200
console.log(response.ok);     // true

// POST with JSON body
const response = await Http
  .withToken(process.env.STRIPE_SECRET!)
  .baseUrl('https://api.stripe.com/v1')
  .contentType('application/x-www-form-urlencoded')
  .post('/customers', 'email=user@example.com&name=John');

Authentication

// Bearer token (default)
Http.withToken('sk_live_...')

// Custom header token (e.g., Postmark uses X-Postmark-Server-Token)
Http.withHeaders({ 'X-Postmark-Server-Token': process.env.POSTMARK_TOKEN! })

// Basic auth
Http.withBasicAuth('username', 'password')

// API key header
Http.withHeaders({ 'X-API-Key': process.env.MAILCHIMP_KEY! })

Third-Party API Examples

Postmark

import { Http } from '@beeblock/svelar/http';

const postmark = Http
  .baseUrl('https://api.postmarkapp.com')
  .withHeaders({ 'X-Postmark-Server-Token': process.env.POSTMARK_TOKEN! });

const response = await postmark.post('/email', {
  From: 'hello@myapp.com',
  To: 'user@example.com',
  Subject: 'Welcome!',
  HtmlBody: '<h1>Hello</h1>',
});

Resend

const resend = Http
  .baseUrl('https://api.resend.com')
  .withToken(process.env.RESEND_API_KEY!);

await resend.post('/emails', {
  from: 'hello@myapp.com',
  to: 'user@example.com',
  subject: 'Welcome!',
  html: '<h1>Hello</h1>',
});

Stripe

const stripe = Http
  .baseUrl('https://api.stripe.com/v1')
  .withToken(process.env.STRIPE_SECRET!)
  .contentType('application/x-www-form-urlencoded');

// Create a customer
const customer = await stripe.post('/customers', 'email=user@example.com');

// List charges with query params
const charges = await stripe.query({ limit: 10 }).get('/charges');

Mailchimp

const mailchimp = Http
  .baseUrl(`https://${process.env.MAILCHIMP_DC}.api.mailchimp.com/3.0`)
  .withToken(process.env.MAILCHIMP_KEY!);

// Get lists
const lists = await mailchimp.get('/lists');

// Add a subscriber
await mailchimp.post(`/lists/${listId}/members`, {
  email_address: 'user@example.com',
  status: 'subscribed',
});

Any REST API

const github = Http
  .baseUrl('https://api.github.com')
  .withToken(process.env.GITHUB_TOKEN!)
  .withHeaders({ 'X-GitHub-Api-Version': '2022-11-28' });

const repos = await github.get('/user/repos');

Retry & Timeout

// Retry up to 3 times on 5xx errors (1s, 2s, 3s delays)
const response = await Http
  .withToken(token)
  .baseUrl('https://api.example.com')
  .retry(3, 1000)
  .timeout(10_000)
  .get('/data');

Only 5xx server errors are retried. Client errors (4xx) fail immediately.

Error Handling

Failed requests throw HttpRequestError with the status code and response body:

import { Http, HttpRequestError } from '@beeblock/svelar/http';

try {
  await Http.withToken(token).baseUrl('https://api.stripe.com/v1').get('/charges');
} catch (err) {
  if (err instanceof HttpRequestError) {
    console.log(err.status); // 401
    console.log(err.body);   // { error: { message: 'Invalid API Key' } }
  }
}

Reusable Clients

Create a pre-configured client in a service file and reuse it across your app:

// src/lib/services/PostmarkClient.ts
import { Http } from '@beeblock/svelar/http';

export const postmarkClient = Http
  .baseUrl('https://api.postmarkapp.com')
  .withHeaders({ 'X-Postmark-Server-Token': process.env.POSTMARK_TOKEN! })
  .retry(2);

// src/lib/services/StripeClient.ts
import { Http } from '@beeblock/svelar/http';

export const stripeClient = Http
  .baseUrl('https://api.stripe.com/v1')
  .withToken(process.env.STRIPE_SECRET!)
  .contentType('application/x-www-form-urlencoded')
  .retry(2);
// In a controller or service
import { postmarkClient } from '$lib/services/PostmarkClient.js';

await postmarkClient.post('/email', {
  From: 'hello@myapp.com',
  To: user.email,
  Subject: 'Your invoice',
  HtmlBody: invoiceHtml,
});

Full API

Method Description
Http.baseUrl(url) Set the base URL for all requests
Http.withToken(token, type?) Set Authorization: Bearer <token> (or custom type)
Http.withBasicAuth(user, pass) Set Authorization: Basic <base64>
Http.withHeaders(headers) Merge additional headers
Http.accept(type) Set Accept header
Http.contentType(type) Set Content-Type header
Http.timeout(ms) Set request timeout (default: 30s)
Http.retry(times, delayMs?) Retry on 5xx errors
Http.query(params) Add URL query parameters
Http.get(path) Send GET request
Http.post(path, body?) Send POST request
Http.put(path, body?) Send PUT request
Http.patch(path, body?) Send PATCH request
Http.delete(path, body?) Send DELETE request

All chainable methods return a new HttpClient instance (immutable) — safe to store and reuse.


Creating Custom Integrations

Svelar provides three levels of extensibility for third-party integrations:

Create a service class that wraps Http for a specific API. This is the simplest and most common pattern:

// src/lib/modules/billing/StripeService.ts
import { Http, HttpClient, HttpRequestError } from '@beeblock/svelar/http';

export class StripeService {
  private client: HttpClient;

  constructor() {
    this.client = Http
      .baseUrl('https://api.stripe.com/v1')
      .withToken(process.env.STRIPE_SECRET!)
      .contentType('application/x-www-form-urlencoded')
      .retry(2);
  }

  async createCustomer(email: string, name?: string) {
    const params = new URLSearchParams({ email });
    if (name) params.set('name', name);
    return this.client.post('/customers', params.toString());
  }

  async createSubscription(customerId: string, priceId: string) {
    const params = new URLSearchParams({
      customer: customerId,
      'items[0][price]': priceId,
    });
    return this.client.post('/subscriptions', params.toString());
  }

  async getInvoices(customerId: string) {
    return this.client.query({ customer: customerId }).get('/invoices');
  }
}

Use it in your controllers:

// src/lib/modules/billing/BillingController.ts
import { Controller } from '@beeblock/svelar/routing';
import { StripeService } from './StripeService.js';

export class BillingController extends Controller {
  private stripe = new StripeService();

  async subscribe(event) {
    const { priceId } = await event.request.json();
    const user = event.locals.user;

    const customer = await this.stripe.createCustomer(user.email, user.name);
    const subscription = await this.stripe.createSubscription(customer.data.id, priceId);

    return this.json({ subscription: subscription.data });
  }
}

2. Custom Mail Driver

To add a new email provider (e.g., Mailchimp Transactional), implement the MailTransport interface:

// src/lib/shared/mail/MailchimpTransport.ts
import { Http } from '@beeblock/svelar/http';
import type { MailTransport, MailMessage, SendResult } from '@beeblock/svelar/mail';

export class MailchimpTransport implements MailTransport {
  private apiKey: string;

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  async send(message: MailMessage): Promise<SendResult> {
    try {
      const response = await Http
        .baseUrl('https://mandrillapp.com/api/1.0')
        .post('/messages/send', {
          key: this.apiKey,
          message: {
            from_email: message.from?.address ?? message.from,
            to: [{ email: message.to, type: 'to' }],
            subject: message.subject,
            html: message.html,
            text: message.text,
          },
        });

      return { success: true, messageId: response.data[0]?._id };
    } catch (err: any) {
      return { success: false, error: err.message };
    }
  }
}

Register it in your app bootstrap:

// src/app.ts
import { Mailer } from '@beeblock/svelar/mail';
import { MailchimpTransport } from '$lib/shared/mail/MailchimpTransport.js';

Mailer.configure({
  default: 'mailchimp',
  mailers: {
    mailchimp: {
      driver: 'custom',
      transport: new MailchimpTransport(process.env.MAILCHIMP_API_KEY!),
    },
  },
});

3. Plugin (For Reusable, Publishable Integrations)

For integrations you want to distribute as an npm package, use the Plugin system:

// svelar-mailchimp/src/index.ts
import { Plugin } from '@beeblock/svelar/plugins';
import { MailchimpTransport } from './MailchimpTransport.js';

export class MailchimpPlugin extends Plugin {
  name = 'svelar-mailchimp';
  version = '1.0.0';
  description = 'Mailchimp Transactional integration for Svelar';

  register() {
    this.app.singleton('mailchimp', () => {
      return new MailchimpTransport(process.env.MAILCHIMP_API_KEY!);
    });
  }

  config() {
    return {
      key: 'mailchimp',
      defaults: {
        apiKey: '',
        defaultFromEmail: 'hello@example.com',
      },
    };
  }
}

Install and use:

// src/app.ts
import { PluginManager } from '@beeblock/svelar/plugins';
import { MailchimpPlugin } from 'svelar-mailchimp';

const plugins = new PluginManager(app);
plugins.use(new MailchimpPlugin());
await plugins.boot();

When to Use What

Pattern When to use
Service App-specific integrations (your Stripe, your CRM, your analytics)
Custom Driver Swappable implementations (new mail provider, new cache backend)
Plugin Publishable packages with migrations, config, and lifecycle hooks

Client-Side: apiFetch

The apiFetch function is a drop-in replacement for fetch that automatically handles CSRF tokens. It reads the XSRF-TOKEN cookie and attaches it as an X-CSRF-Token header on mutation requests (POST, PUT, PATCH, DELETE).

Import

import { apiFetch } from '@beeblock/svelar/http';

Basic Usage

// GET request (no CSRF token needed)
const res = await apiFetch('/api/posts');
const posts = await res.json();

// POST request (CSRF token auto-attached)
const res = await apiFetch('/api/posts', {
  method: 'POST',
  body: JSON.stringify({ title: 'Hello', body: 'World' }),
});

// PUT request
await apiFetch(`/api/posts/${id}`, {
  method: 'PUT',
  body: JSON.stringify({ title: 'Updated' }),
});

// DELETE request
await apiFetch(`/api/posts/${id}`, { method: 'DELETE' });

In Svelte Components

<script lang="ts">
  import { apiFetch } from '@beeblock/svelar/http';

  let posts: any[] = $state([]);
  let loading = $state(false);

  async function loadPosts() {
    loading = true;
    const res = await apiFetch('/api/posts');
    posts = await res.json();
    loading = false;
  }

  async function createPost(title: string, body: string) {
    const res = await apiFetch('/api/posts', {
      method: 'POST',
      body: JSON.stringify({ title, body }),
    });

    if (res.ok) {
      await loadPosts(); // Refresh
    }
  }
</script>

Custom CSRF Configuration

// Custom cookie/header names
await apiFetch('/api/data', {
  method: 'POST',
  body: JSON.stringify({ key: 'value' }),
  csrfCookieName: 'MY_CSRF_TOKEN',
  csrfHeaderName: 'X-My-Csrf',
});

How It Works

Svelar's CsrfMiddleware sets an XSRF-TOKEN cookie on every safe request (GET, HEAD, OPTIONS). When apiFetch makes a mutation request, it reads that cookie and sends it back as a header. The middleware validates the header matches the cookie — this is the "double-submit cookie" pattern.

Safe methods (GET, HEAD, OPTIONS) skip CSRF entirely. If the body is a string and no Content-Type is set, apiFetch defaults to application/json.

buildUrl

Helper to construct URLs with query parameters:

import { buildUrl } from '@beeblock/svelar/http';

buildUrl('/api/posts', { page: 1, per_page: 10 });
// => '/api/posts?page=1&per_page=10'

buildUrl('/api/users', { role: 'admin', active: true });
// => '/api/users?role=admin&active=true'

// Null/undefined values are omitted
buildUrl('/api/posts', { page: 1, search: undefined });
// => '/api/posts?page=1'

getCsrfToken

Extract the CSRF token directly:

import { getCsrfToken } from '@beeblock/svelar/http';

const token = getCsrfToken();
// Returns null on the server (SSR)

Next Steps

  • Set up Internationalization for multi-language support
  • Explore Forms for server-side validation with Superforms
  • Learn about Middleware to understand how CSRF protection works
  • See Mail for built-in email drivers (SMTP, Postmark, Resend)
  • Check Plugins for building publishable integrations

Svelar HTTP Guide © 2026

Svelar © 2026 · MIT License