Skip to content

Use the Pastepile API from React

A React hook for creating pastes with fetch: AbortController cleanup, the API error envelope, 429 handling with Retry-After, and a working share form.

Updated

This guide adds create-a-share-link to a React app using the Pastepile paste API and nothing else: no client library, no backend, no state manager. The API sends CORS headers on every response, so plain fetch from the browser is the whole integration.

The CORS behavior you are relying on

Every response from the API carries Access-Control-Allow-Origin: *, including errors, 404s, and rate-limit responses, so a failed request is a readable failure instead of an opaque CORS error in the console. Preflight (OPTIONS) requests are answered even for unknown paths. Authentication is header-based; there are no cookies, so do not set credentials: "include" on your requests. The rate-limit headers (Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) and X-Request-Id are exposed cross-origin, so browser code can read them.

A useCreatePaste hook

The hook owns the request lifecycle: it aborts an in-flight request when a new one starts or the component unmounts, distinguishes rate limiting from other failures, and surfaces the API's own error message rather than a generic one.

useCreatePaste.ts
import { useCallback, useEffect, useRef, useState } from "react";

export interface CreatePasteResponse {
  slug: string;
  url: string;
  raw_url: string;
  /** Shown once. Needed to update or delete the paste later. */
  edit_key: string;
  expires_at?: string;
}

interface ApiErrorBody {
  error?: { code?: string; message?: string; retry_after?: number };
}

export function useCreatePaste() {
  const [result, setResult] = useState<CreatePasteResponse | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [pending, setPending] = useState(false);
  const abortRef = useRef<AbortController | null>(null);

  useEffect(() => () => abortRef.current?.abort(), []);

  const create = useCallback(async (content: string) => {
    abortRef.current?.abort();
    const controller = new AbortController();
    abortRef.current = controller;
    setPending(true);
    setError(null);
    try {
      const res = await fetch("https://www.pastepile.com/api/public/pastes", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          content,
          expiry: "1w",
          visibility: "unlisted",
        }),
        signal: controller.signal,
      });
      const body = (await res.json()) as CreatePasteResponse & ApiErrorBody;
      if (!res.ok) {
        if (res.status === 429) {
          const wait =
            res.headers.get("Retry-After") ??
            String(body.error?.retry_after ?? 60);
          throw new Error(
            "Rate limited. Try again in " + wait + " seconds.",
          );
        }
        throw new Error(
          body.error?.message ?? "Request failed (" + res.status + ")",
        );
      }
      setResult(body);
    } catch (e) {
      if (e instanceof DOMException && e.name === "AbortError") return;
      setError(e instanceof Error ? e.message : "Request failed");
    } finally {
      setPending(false);
    }
  }, []);

  return { create, result, error, pending };
}

Using it in a form

ShareForm.tsx
import { useState } from "react";
import { useCreatePaste } from "./useCreatePaste";

export function ShareForm() {
  const [text, setText] = useState("");
  const { create, result, error, pending } = useCreatePaste();

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        if (text.trim()) create(text);
      }}
    >
      <textarea
        value={text}
        onChange={(e) => setText(e.target.value)}
        aria-label="Text to share"
      />
      <button type="submit" disabled={pending || !text.trim()}>
        {pending ? "Creating link..." : "Create share link"}
      </button>
      {result && (
        <p>
          Link: <a href={result.url}>{result.url}</a>
        </p>
      )}
      {error && <p role="alert">{error}</p>}
    </form>
  );
}

React-specific notes: keep the create call inside an event handler, not an effect. If you do fetch a paste on mount (a read of GET /api/public/pastes/:slug), remember that development StrictMode mounts components twice, so an unguarded effect fires the request twice; the AbortController pattern above handles that correctly for reads too. The edit_key in the response is shown exactly once, so if your app offers delete-my-paste later, persist it at creation time.

Keys, limits, and what stays free

Creating pastes through the API without a key works until September 8, 2026. From that date, API writes need an active subscription and an API key. Reads stay free and keyless, and pasting in the browser at pastepile.com stays free; only programmatic writes are affected.

An API key is a secret. Do not ship one inside a public frontend bundle, where anyone can read it from the source. For an app whose users write pastes after the keyless window closes, either let each user paste their own key at runtime (stored client-side only), or route write calls through a minimal backend you control that holds the key.

Limits that matter from a browser app: keyless creates are limited to 30 per hour per IP address, with a burst cap of 30 per minute. Keyless reads allow 120 per minute per IP. The maximum paste size is 2 MB without a key and 25 MB with a Pro key. The full table lives in the API docs.

Read the full API docs

Related

Other developer guides