---
title: "mixFetch Example: Private HTTP Requests"
description: "Replace browser fetch with mixFetch to route HTTP requests through the Nym mixnet. Covers setup, CA certificates, WSS gateways, and usage examples."
schemaType: "TechArticle"
section: "Developers"
lastUpdated: "2026-03-15"
---

import { Callout } from 'nextra/components';

# mixFetch

An easy way to secure parts or all of your web app is to replace calls to [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) with `mixFetch`. It works the same as vanilla `fetch`: it's a proxied wrapper around the original function.

Things to be aware of:

- CA certificates in `mixFetch` are periodically updated. If you get a certificate error, the root certificate you need might not be valid yet. [Send a PR](https://github.com/nymtech/nym/pulls) if you need changes to the certificates.
- If you are using `mixFetch` in a web app with HTTPS, you will need to use a gateway that has Secure Websockets (WSS) to avoid a [mixed content](https://developer.mozilla.org/en-US/docs/Web/Security/Mixed_content) error.
- `mixFetch` supports concurrent requests (up to 10) to the same or different URLs.

<Callout type="info">
Right now Gateways are not required to run a Secure Websocket (WSS) listener, so only a subset of nodes running in Gateway mode have configured their nodes to do so. You need to select a Gateway that has WSS from [Harbourmaster](https://harbourmaster.nymtech.net/).
</Callout>

## Environment Setup

Create a new project with Vite:

```bash
npm create vite@latest
```

Choose React + TypeScript, then:

```bash
cd <YOUR_APP>
npm i
npm run dev
```

## Installation

```bash
npm install @nymproject/mix-fetch-full-fat
```

## Configuration

```ts
import type { SetupMixFetchOps } from '@nymproject/mix-fetch-full-fat';

const mixFetchOptions: SetupMixFetchOps = {
  clientId: "docs-mixfetch-demo",
  preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1",
  mixFetchOverride: {
    requestTimeoutMs: 60_000,
  },
  forceTls: true, // force WSS
};
```

## Full Example

This example shows explicit initialization via `createMixFetch`, single URL fetch, and concurrent requests. Results appear both in the UI and in a visible log panel.

<Callout type="info">
For this example we use the `full-fat` version of the ESM SDK. If you use the unbundled ESM variant, make sure your [bundler configuration](../bundling/bundling) copies the WASM and web worker files to the output bundle.
</Callout>

```tsx
import React, { useState, useRef, useEffect } from "react";
import CircularProgress from "@mui/material/CircularProgress";
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { mixFetch, createMixFetch } from "@nymproject/mix-fetch-full-fat";
import Stack from "@mui/material/Stack";
import Paper from "@mui/material/Paper";
import type { SetupMixFetchOps } from "@nymproject/mix-fetch-full-fat";

const defaultUrl =
  "https://nymtech.net/.wellknown/network-requester/exit-policy.txt";
const args = { mode: "unsafe-ignore-cors" };
const mixFetchOptions: SetupMixFetchOps = {
  clientId: "docs-mixfetch-demo",
  preferredGateway: "q2A2cbooyC16YJzvdYaSMH9X3cSiieZNtfBr8cE8Fi1",
  mixFetchOverride: {
    requestTimeoutMs: 60_000,
  },
  forceTls: true,
};

// Log entry type for the visible log panel
type LogLevel = "info" | "error" | "send" | "receive";
type LogEntry = { timestamp: string; message: string; level: LogLevel };

const logColors: Record<LogLevel, string> = {
  info: "gray",
  error: "red",
  send: "blue",
  receive: "green",
};

const logLabels: Record<LogLevel, string> = {
  info: "INFO",
  error: "ERROR",
  send: "SEND",
  receive: "RECV",
};

export const MixFetch = () => {
  // MixFetch initialization state
  const [status, setStatus] = useState<"idle" | "starting" | "ready" | "error">("idle");
  const [errorMsg, setErrorMsg] = useState<string | null>(null);

  // Log panel state
  const [logs, setLogs] = useState<LogEntry[]>([]);
  const logEndRef = useRef<HTMLDivElement>(null);

  // Single fetch state
  const [url, setUrl] = useState<string>(defaultUrl);
  const [html, setHtml] = useState<string>();
  const [busy, setBusy] = useState<boolean>(false);

  // Concurrent fetch state
  const [concurrentResults, setConcurrentResults] = useState<string[]>([]);
  const [concurrentBusy, setConcurrentBusy] = useState<boolean>(false);

  // Auto-scroll log panel to bottom
  useEffect(() => {
    logEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [logs]);

  // Helper to add a timestamped log entry
  const addLog = (message: string, level: LogLevel) => {
    const timestamp = new Date().toISOString().substring(11, 23);
    setLogs((prev) => [...prev, { timestamp, message, level }]);
  };

  // Initialize MixFetch explicitly via createMixFetch
  const handleStart = async () => {
    try {
      setStatus("starting");
      setErrorMsg(null);
      addLog("Starting MixFetch...", "info");
      await createMixFetch(mixFetchOptions);
      setStatus("ready");
      addLog("MixFetch is ready!", "info");
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      setStatus("error");
      setErrorMsg(msg);
      addLog(`Error: ${msg}`, "error");
    }
  };

  // Single URL fetch — reuses the existing MixFetch singleton
  const handleFetch = async () => {
    try {
      setBusy(true);
      setHtml(undefined);
      addLog(`Sending request to ${url}...`, "send");
      const response = await mixFetch(url, args, mixFetchOptions);
      const resHtml = await response.text();
      setHtml(resHtml);
      addLog(`Response received (${resHtml.length} bytes)`, "receive");
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      addLog(`Fetch error: ${msg}`, "error");
    } finally {
      setBusy(false);
    }
  };

  // Send 5 concurrent requests to different URLs on the same domain
  const handleConcurrentFetch = async () => {
    const baseUrl = "https://jsonplaceholder.typicode.com/posts/";
    const count = 5;
    try {
      setConcurrentBusy(true);
      setConcurrentResults([]);
      addLog(
        `Starting ${count} concurrent requests to ${baseUrl}1-${count}...`,
        "send",
      );
      const requests = Array.from({ length: count }, (_, i) => {
        const targetUrl = `${baseUrl}${i + 1}`;
        return mixFetch(targetUrl, args, mixFetchOptions)
          .then((res) => res.json())
          .then((json: { id: number; title: string }) => {
            const entry = `[${json.id}] ${json.title}`;
            addLog(entry, "receive");
            return entry;
          });
      });
      const results = await Promise.all(requests);
      setConcurrentResults(results);
      addLog(`All ${count} concurrent requests completed!`, "info");
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      addLog(`Concurrent fetch error: ${msg}`, "error");
    } finally {
      setConcurrentBusy(false);
    }
  };

  const isReady = status === "ready";

  return (
    <div style={{ marginTop: "1rem" }}>
      {/* Start MixFetch */}
      <Paper sx={{ p: 2, mb: 2 }} variant="outlined">
        <Stack direction="row" alignItems="center" spacing={2}>
          <Button
            variant="contained"
            disabled={status === "starting" || status === "ready"}
            onClick={handleStart}
          >
            Start MixFetch
          </Button>
          {status === "starting" && <CircularProgress size={20} />}
          <Typography fontFamily="monospace" fontSize="small">
            {status === "idle" ? "Not started" :
             status === "starting" ? "Starting..." :
             status === "ready" ? "Ready" :
             `Error: ${errorMsg}`}
          </Typography>
        </Stack>
      </Paper>

      {/* Fetch controls — disabled until MixFetch is ready */}
      <Box sx={{ opacity: isReady ? 1 : 0.5, pointerEvents: isReady ? "auto" : "none" }}>
        {/* Single fetch */}
        <Stack direction="row">
          <TextField
            disabled={busy}
            fullWidth
            label="URL"
            type="text"
            variant="outlined"
            defaultValue={defaultUrl}
            onChange={(e) => setUrl(e.target.value)}
          />
          <Button variant="outlined" disabled={busy} sx={{ marginLeft: "1rem" }} onClick={handleFetch}>
            Fetch
          </Button>
        </Stack>
        {busy && <Box mt={2}><CircularProgress /></Box>}
        {html && (
          <>
            <Box mt={2}><strong>Response</strong></Box>
            <Paper sx={{ p: 2, mt: 1 }} elevation={4}>
              <Typography fontFamily="monospace" fontSize="small">{html}</Typography>
            </Paper>
          </>
        )}

        {/* Concurrent fetch */}
        <Box mt={3}>
          <strong>Concurrent Requests</strong>
          <Box mt={1}>
            <Button variant="outlined" disabled={concurrentBusy} onClick={handleConcurrentFetch}>
              Send 5 Concurrent Requests (posts/1-5)
            </Button>
          </Box>
        </Box>
        {concurrentBusy && <Box mt={2}><CircularProgress /></Box>}
        {concurrentResults.length > 0 && (
          <Paper sx={{ p: 2, mt: 2 }} elevation={4}>
            {concurrentResults.map((result, i) => (
              <Typography key={i} fontFamily="monospace" fontSize="small">{result}</Typography>
            ))}
          </Paper>
        )}
      </Box>

      {/* Log Panel */}
      {logs.length > 0 && (
        <Paper sx={{ p: 2, mt: 3, maxHeight: 200, overflow: "auto" }} variant="outlined">
          <strong>Log</strong>
          {logs.map((entry, i) => (
            <Typography key={i} fontFamily="monospace" fontSize="small" sx={{ color: logColors[entry.level] }}>
              {entry.timestamp} [{logLabels[entry.level]}] {entry.message}
            </Typography>
          ))}
          <div ref={logEndRef} />
        </Paper>
      )}
    </div>
  );
};
```
