Use the Pastepile API from Angular
Create and read pastes from an Angular app with HttpClient: typed responses, the error envelope, 429 handling with Retry-After, and no backend required.
Updated
This guide wires the Pastepile paste API into an Angular application with HttpClient: a typed service, create and read calls, real error handling for the API's error envelope, and rate-limit handling that respects Retry-After. It exists because an Angular developer went looking for a third-party API that actually works with browser CORS, and most do not.
Why this works from the browser at all
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.
That means no dev-server proxy configuration, no CORS browser extensions, and no backend whose only job is to forward requests. HttpClient talks to the API directly.
A typed paste service
With standalone components (Angular 17 and later), provide HttpClient once in your app config:
import { ApplicationConfig } from "@angular/core";
import { provideHttpClient } from "@angular/common/http";
export const appConfig: ApplicationConfig = {
providers: [provideHttpClient()],
};Then the service. The interfaces below match the live contract of POST /api/public/pastes and GET /api/public/pastes/:slug; nothing is invented.
import { HttpClient } from "@angular/common/http";
import { inject, Injectable } from "@angular/core";
export interface CreatePasteRequest {
title?: string;
content: string;
expiry?: "10m" | "1h" | "1d" | "1w" | "1mo" | "burn" | "never";
visibility?: "public" | "unlisted";
}
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;
}
export interface PasteFile {
name: string;
content: string;
language: string;
}
export interface GetPasteResponse {
slug: string;
title: string;
language: string;
files: PasteFile[];
created_at: string;
expires_at: string | null;
burn_after_read: boolean;
}
@Injectable({ providedIn: "root" })
export class PasteService {
private http = inject(HttpClient);
private base = "https://www.pastepile.com";
create(req: CreatePasteRequest) {
return this.http.post<CreatePasteResponse>(this.base + "/api/public/pastes", req);
}
get(slug: string) {
return this.http.get<GetPasteResponse>(this.base + "/api/public/pastes/" + slug);
}
}Handling the error envelope and 429s
Every error is JSON of the shape { "error": { "code", "message", "request_id" } }. Rate-limit errors use the code rate_limited, add retry_after (seconds) to the body, and set a Retry-After header that Angular can read from the HttpErrorResponse. A component using the service:
import { HttpErrorResponse } from "@angular/common/http";
interface ApiErrorBody {
error?: { code?: string; message?: string; retry_after?: number };
}
// inside the component class:
share() {
this.pending = true;
this.pastes
.create({ content: this.text, expiry: "1w", visibility: "unlisted" })
.subscribe({
next: (res) => {
this.pending = false;
this.shareUrl = res.url;
// res.edit_key is shown once; keep it if you want to
// update or delete this paste later.
},
error: (err: HttpErrorResponse) => {
this.pending = false;
const body = (err.error ?? {}) as ApiErrorBody;
if (err.status === 429) {
const seconds =
Number(err.headers.get("Retry-After")) ||
body.error?.retry_after ||
60;
this.message =
"Rate limited. Try again in " + seconds + " seconds.";
return;
}
this.message = body.error?.message ?? "Request failed.";
},
});
}Do not retry a 429 in a loop without waiting: the correct wait is exactly what Retry-After says. For background work, an RxJS timer(seconds * 1000) followed by one retry is enough; for user-initiated actions, showing the wait time is usually better than retrying silently.
X-Request-Id response header (or error.request_id in the body) and include it when you contact us. It identifies that exact request and contains nothing about you.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.
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.
Behavior worth knowing before you ship
visibility: "unlisted"keeps a paste off the public archive; it is reachable by anyone who has the link. For content only the recipient should read, create the paste in the browser with end-to-end encryption instead; the API cannot encrypt for you.expiry: "burn"destroys the paste on first read, including a read by a link-preview bot. See self-destructing pastes.- The
edit_keyin the create response is shown once and never again. Store it if you need to update or delete the paste programmatically. - Password-protected and end-to-end encrypted pastes are not readable through the JSON read endpoint, by design.