+ Integration Guide

· Updated August 28, 2026

Reddit Scraping API with TypeScript

How to search Reddit and scrape posts using TypeScript with full type safety. Working examples with typed responses for every FetchLayer endpoint.

Written by Alex P.

  • TypeScript
  • reddit scraping
  • reddit API
  • API integration
  • type safety

This guide shows you how to use the FetchLayer Reddit API with TypeScript. You get full type safety on API responses with zero external dependencies.

If you want the official TypeScript package instead of defining the response types yourself, FetchLayer now ships @fetchlayer/reddit. It includes typed response interfaces for all 15 endpoints and is open source on GitHub. For the package-first version, see Reddit API npm Package for JavaScript & TypeScript.


Setup

  1. Get a free API key (no credit card)
  2. TypeScript 5+ and Node 18+ (or Bun/Deno)

Optional, if you want the official SDK with built-in endpoint types:

npm install @fetchlayer/reddit

Type Definitions

Define the response types for the endpoints you’ll use:

interface RedditPost {
  title: string;
  subreddit: string;
  author: string;
  score: number;
  numComments: number;
  url: string;
  selftext?: string;
  createdUtc: number;
}

interface SearchResponse {
  results: RedditPost[];
}

interface CommunityPostsResponse {
  subreddit: string;
  posts: RedditPost[];
}

interface PostResponse {
  title: string;
  author: string;
  score: number;
  numComments: number;
  selftext: string;
  url: string;
  comments: RedditComment[];
}

interface RedditComment {
  author: string;
  body: string;
  score: number;
  createdUtc: number;
  replies?: RedditComment[];
}

interface UserProfile {
  username: string;
  displayName: string;
  totalKarma: number;
  accountAge: string;
  isVerified: boolean;
}

interface CommunityResult {
  name: string;
  title: string;
  subscribers: number;
  description: string;
}

interface SearchCommunitiesResponse {
  results: CommunityResult[];
}

Typed API Client

const API_KEY = process.env.FETCHLAYER_API_KEY!;
const BASE_URL = 'https://api.fetchlayer.dev/reddit';

async function reddit<T>(endpoint: string, body: Record<string, unknown>): Promise<T> {
  const res = await fetch(`${BASE_URL}/${endpoint}`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    throw new Error(`FetchLayer ${res.status}: ${await res.text()}`);
  }

  return res.json() as Promise<T>;
}

Search Reddit

const data = await reddit<SearchResponse>('search', {
  query: 'best CI/CD tools',
  sort: 'top',
  limit: 10,
});

// Full type safety — TypeScript knows data.results is RedditPost[]
for (const post of data.results) {
  console.log(`${post.title} — r/${post.subreddit} — ${post.score} pts`);
}

Get Subreddit Posts

const data = await reddit<CommunityPostsResponse>('community-posts', {
  subreddit: 'typescript',
  sort: 'top',
  time: 'month',
  limit: 20,
});

const highScoring = data.posts.filter(p => p.score > 100);
console.log(`${highScoring.length} posts with 100+ upvotes`);

Scrape a Post with Comments

const thread = await reddit<PostResponse>('post', {
  url: 'https://www.reddit.com/r/programming/comments/abc123/some_post/',
  pages: 2,
});

console.log(`${thread.title} — ${thread.comments.length} top-level comments`);

// Recursively count all comments including nested replies
function countComments(comments: RedditComment[]): number {
  return comments.reduce(
    (sum, c) => sum + 1 + (c.replies ? countComments(c.replies) : 0),
    0
  );
}

console.log(`Total comments: ${countComments(thread.comments)}`);

Get User Profile

const profile = await reddit<UserProfile>('user-profile', {
  username: 'spez',
});

console.log(`${profile.username} — ${profile.totalKarma} karma — ${profile.accountAge}`);

Search Subreddits

const communities = await reddit<SearchCommunitiesResponse>('search-communities', {
  query: 'machine learning',
});

const sorted = communities.results.sort((a, b) => b.subscribers - a.subscribers);
for (const sub of sorted) {
  console.log(`r/${sub.name} — ${sub.subscribers.toLocaleString()} subscribers`);
}

Full Example: Typed Reddit Monitoring

interface MonitorConfig {
  keyword: string;
  subreddits: string[];
  minScore: number;
}

async function monitor(config: MonitorConfig): Promise<void> {
  console.log(`Monitoring "${config.keyword}"...\n`);

  for (const sub of config.subreddits) {
    const data = await reddit<SearchResponse>('search', {
      query: config.keyword,
      subreddit: sub,
      sort: 'new',
      limit: 10,
    });

    const relevant = data.results.filter(p => p.score >= config.minScore);

    if (relevant.length > 0) {
      console.log(`--- r/${sub} (${relevant.length} matches) ---`);
      for (const post of relevant) {
        console.log(`  [${post.score}] ${post.title}`);
      }
      console.log();
    }
  }
}

await monitor({
  keyword: 'your-brand',
  subreddits: ['startups', 'SaaS', 'webdev'],
  minScore: 5,
});

Handling Errors and Rate Limits

The typed client above throws a generic error for any non-OK response. FetchLayer returns standard HTTP status codes: 401 for a missing or invalid API key, 400 when a required field like query or subreddit is missing, and 429 once you exceed your plan’s request rate — worth its own error type so callers can catch and branch on it with instanceof:

class RateLimitError extends Error {}

async function reddit<T>(endpoint: string, body: Record<string, unknown>, retries = 2): Promise<T> {
  for (let attempt = 0; attempt <= retries; attempt++) {
    const res = await fetch(`${BASE_URL}/${endpoint}`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });

    if (res.status === 429) {
      if (attempt === retries) throw new RateLimitError('Rate limited — back off and try again later');
      const wait = Number(res.headers.get('Retry-After') ?? 5);
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    if (res.status === 401) throw new Error('Invalid or missing API key');
    if (res.status === 400) throw new Error(`Bad request to /${endpoint}: ${await res.text()}`);
    if (!res.ok) throw new Error(`FetchLayer ${res.status}: ${await res.text()}`);

    return res.json() as Promise<T>;
  }
  throw new RateLimitError('Rate limited — back off and try again later');
}

A 429 mid-run is expected behavior on the free tier, not a bug — the retry above respects Retry-After rather than hammering the endpoint again immediately. Catching RateLimitError separately from a plain Error lets a caller back off a rate limit differently from surfacing an auth or validation failure.


Types are not validation

The interfaces above describe what you expect. They’re erased at compile time and check nothing at runtime — res.json() returns any, and casting it to SearchResponse is an assertion, not a guarantee. If a field is missing, TypeScript is happy and your code fails later, somewhere less obvious.

For a script that’s fine. For anything scheduled, validate at the boundary with zod:

npm install zod
import { z } from 'zod';

const RedditPostSchema = z.object({
  title: z.string(),
  subreddit: z.string(),
  score: z.number().default(0),
  numComments: z.number().default(0),
  url: z.string().url(),
  author: z.string().optional(),
});

const SearchResponseSchema = z.object({
  results: z.array(RedditPostSchema),
});

// Infer the TS type from the schema so there's one source of truth
// instead of an interface and a validator that drift apart.
export type RedditPost = z.infer<typeof RedditPostSchema>;

async function redditValidated<T extends z.ZodTypeAny>(
  endpoint: string,
  schema: T,
  body: Record<string, unknown>,
): Promise<z.infer<T>> {
  const res = await fetch(`${BASE}/${endpoint}`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });

  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);

  const parsed = schema.safeParse(await res.json());
  if (!parsed.success) {
    // Fail at the boundary with a precise path, rather than
    // three functions later with "cannot read property of undefined".
    throw new Error(`Unexpected response from /${endpoint}: ${parsed.error.message}`);
  }
  return parsed.data;
}

const { results } = await redditValidated('search', SearchResponseSchema, {
  query: 'typescript generics',
  sort: 'top',
  limit: 10,
});

z.infer is the key move: derive the type from the schema instead of maintaining an interface alongside a validator. They can’t disagree if only one of them exists.


Discriminated unions for endpoint responses

Different Reddit endpoints return different top-level keys — search gives results, community-posts gives posts. A generic reddit<T>() will happily let you read the wrong one:

// Compiles. Returns undefined at runtime.
const data = await reddit<CommunityPostsResponse>('search', { query: 'x' });
console.log(data.posts.length);

Bind the endpoint name to its response type with a map, so the wrong pairing stops compiling:

interface EndpointMap {
  'search': { results: RedditPost[] };
  'community-posts': { posts: RedditPost[] };
  'user-posts': { posts: RedditPost[] };
  'search-communities': { communities: { name: string; subscribers: number }[] };
}

async function reddit<E extends keyof EndpointMap>(
  endpoint: E,
  body: Record<string, unknown>,
): Promise<EndpointMap[E]> {
  const res = await fetch(`${BASE}/${endpoint}`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  return res.json() as Promise<EndpointMap[E]>;
}

const search = await reddit('search', { query: 'x' });
search.results;  // ✅ typed as RedditPost[]
search.posts;    // ❌ Property 'posts' does not exist

const community = await reddit('community-posts', { subreddit: 'webdev' });
community.posts; // ✅

You also get autocomplete on the endpoint name itself, which removes the other common bug in this code — a typo’d endpoint string that only fails when the request 404s.


What’s Next