Developers
Step-by-step Examples
1. mixFetch

mixFetch

An easy way to secure parts or all of your web app is to replace calls to fetch (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) error.
  • mixFetch supports concurrent requests (up to 10) to the same or different URLs.

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 (opens in a new tab).

Environment Setup

Create a new project with Vite:

npm create vite@latest

Choose React + TypeScript, then:

cd <YOUR_APP>
npm i
npm run dev

Installation

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

Configuration

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.

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 copies the WASM and web worker files to the output bundle.

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>
  );
};