Skip to content

Fetching Data from the Reddit API

Eventually we are going to implement a rather complex stream to deal with fetching data from Reddit — this will include pagination and automatic retries to fetch more content if we didn’t find enough GIFs. But, to begin with, we are going to keep things simple. We are just going to make a single request to the Reddit API to retrieve data and we will save paging until the next lesson.

Create the Interfaces

We like to know what data is available in our objects. We have used this multiple times now where we have an interface so that we can enforce that something has certain properties. This way, TypeScript will warn us when we have done something wrong and we can also get our data auto completing in the template.

We are about to pull in some data from Reddit. We have our Gif interface already, but that is the target object we want to create from the data we pull in, the data that Reddit returns is a bit different.

We could just pull in the data from Reddit, give it an any type, and not worry about it. But let’s say we then access some data like:

data.title

If we just give our data any type then it will assume the value is valid, regardless of whether or not that property exists. So, first, we are going to create some interfaces that define the “shape” of the data returned from Reddit.

export interface RedditPost {
data: RedditPostData;
}
interface RedditPostData {
author: string;
name: string;
permalink: string;
preview: RedditPreview;
secure_media: RedditMedia;
title: string;
media: RedditMedia;
url: string;
thumbnail: string;
num_comments: number;
}
interface RedditPreview {
reddit_video_preview: RedditVideoPreview;
}
interface RedditVideoPreview {
is_gif: boolean;
fallback_url: string;
}
interface RedditMedia {
reddit_video: RedditVideoPreview;
}

We really just want one interface called RedditPost here, but that data is an object that contains multiple children. Some of those children also contain additional objects. What we need to do is inspect the response returned from making a request to the Reddit API:

https://www.reddit.com/r/gifs/hot/.json?limit=100

And for all the simple values like strings and numbers we can just add directly to our interface. But, when we run into another object, we need to create a separate interface that is then used in our parent interface. This is why we end up with such a complex structure that in a general sense looks like this:

RedditPost {
RedditPostData {
RedditMedia {}
RedditPreview {
RedditVideoPreview
}
}
}

We drill down through the structure returned from the API and map it out in our interface.

import { RedditPost } from './reddit-post';
export interface RedditResponse {
data: RedditResponseData;
}
interface RedditResponseData {
children: RedditPost[];
}

The RedditPost data we want is not the only data that is returned from Reddit, but it is all we are interested in. The purpose of this interface is to be a container for the response returned from Reddit.

export * from './gif';
export * from './reddit-post';
export * from './reddit-response';

Making a real HTTP request

Before we can make HTTP requests, we need to add the HttpClientModule to our application. See if you can remember how to do that, but I will also post the solution below.

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideHttpClient } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideAnimationsAsync(),
provideHttpClient(),
],
};
import { Injectable, computed, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { Gif, RedditPost, RedditResponse } from '../interfaces';
import { EMPTY, catchError, map } from 'rxjs';
export interface GifsState {
gifs: Gif[];
}
@Injectable({ providedIn: 'root' })
export class RedditService {
private http = inject(HttpClient);
// state
private state = signal<GifsState>({
gifs: [],
});
// selectors
gifs = computed(() => this.state().gifs);
//sources
private gifsLoaded$ = this.fetchFromReddit('gifs');
constructor() {
//reducers
this.gifsLoaded$.pipe(takeUntilDestroyed()).subscribe((gifs) =>
this.state.update((state) => ({
...state,
gifs: [...state.gifs, ...gifs],
}))
);
}
private fetchFromReddit(subreddit: string) {
return this.http
.get<RedditResponse>(
`https://www.reddit.com/r/${subreddit}/hot/.json?limit=100`
)
.pipe(
catchError((err) => EMPTY),
map((response) => this.convertRedditPostsToGifs(response.data.children))
);
}
private convertRedditPostsToGifs(posts: RedditPost[]) {
// TODO: Implement
}
private getBestSrcForGif(post: RedditPost) {
// TODO: Implement
}
}

We have the same general outline for our state management, but we have set up a little bit of extra structure here.

Our request to Reddit is going to become quite complex, so we have separated it out into a separate fetchFromReddit method that accepts the subreddit we want to fetch GIFs from as a parameter. Since we are dealing with an HTTP request, we have to consider that it might fail. We are dealing with this in a simple way here with catchError. If there is an error, we will catch it and return the EMPTY stream instead which will stop the stream from breaking entirely.

Basically, this strategy just ignores errors as if they never happened — if a user makes a request to a subreddit that does not exist, they will just get no results. Later, we will handle errors properly by actually displaying something to the user.

We also map the response we get from our HTTP request because we don’t want all of the raw data — we want to convert it into our specific format for a RedditPost. To do this, we will first pass it to the convertRedditPostsToGifs method which will handle most of the data formatting. There is another tricky aspect here though, and that is that there are multiple different types of media that might be used for a “GIF”. Our getBestSrcForGif method will handle searching through the data and returning the best available src for our video tag.

Again, there is a lot more manual handling than is required typically for applications. Usually when you are working with an API it will return data in the format you need — in this case, we are using an API in a non standard way. This does give us good practice for dealing with more complex scenarios in our application though.

Let’s finish implementing those placeholder methods now.

private convertRedditPostsToGifs(posts: RedditPost[]) {
const defaultThumbnails = ['default', 'none', 'nsfw'];
return posts
.map((post) => {
const thumbnail = post.data.thumbnail;
const modifiedThumbnail = defaultThumbnails.includes(thumbnail)
? `/assets/${thumbnail}.png`
: thumbnail;
const validThumbnail =
modifiedThumbnail.endsWith('.jpg') ||
modifiedThumbnail.endsWith('.png');
return {
src: this.getBestSrcForGif(post),
author: post.data.author,
name: post.data.name,
permalink: post.data.permalink,
title: post.data.title,
thumbnail: validThumbnail ? modifiedThumbnail : `/assets/default.png`,
comments: post.data.num_comments,
};
})
.filter((post): post is Gif => post.src !== null);
}

All we are doing here is taking values from the RedditPost array that the API returns, and we are converting it into our own Gif format. If the Gif does not have a src then we filter it out. Note that we are also mapping the thumbnail to different values here — this is so that later we can supply our own images for certain types of thumbnails.

private getBestSrcForGif(post: RedditPost) {
// If the source is in .mp4 format, leave unchanged
if (post.data.url.indexOf('.mp4') > -1) {
return post.data.url;
}
// If the source is in .gifv or .webm formats, convert to .mp4 and return
if (post.data.url.indexOf('.gifv') > -1) {
return post.data.url.replace('.gifv', '.mp4');
}
if (post.data.url.indexOf('.webm') > -1) {
return post.data.url.replace('.webm', '.mp4');
}
// If the URL is not .gifv or .webm, check if media or secure media is available
if (post.data.secure_media?.reddit_video) {
return post.data.secure_media.reddit_video.fallback_url;
}
if (post.data.media?.reddit_video) {
return post.data.media.reddit_video.fallback_url;
}
// If media objects are not available, check if a preview is available
if (post.data.preview?.reddit_video_preview) {
return post.data.preview.reddit_video_preview.fallback_url;
}
// No useable formats available
return null;
}

This is just kind of an ugly method because the Reddit API is quite awkward to work with in this manner. There are all sorts of different types of properties the media might be stored under, so we are checking each of those in order and returning the media if it is available — otherwise we return null.

Displaying the GIFs

Ok, obviously we need to do something about this invisible videos situation. The GifPlayerComponent actually works quite well at this point — we can load the videos successfully, we will see the loading spinner display, and the video will start playing when it has loaded. We can also click the video again to pause it.

But wouldn’t it be nice if we could actually see the videos first?

What we are going to do now is make use of the GIFs thumbnail property so that we have a preview of the GIF before we actually play it.

<div
class="preload-background"
[style.background]="'url(' + thumbnail() + ') 50% 50% / cover no-repeat'"
[class.blur]="
status() !== 'loaded' &&
!['/assets/nsfw.png', '/assets/default.png'].includes(thumbnail())
"
>
<video
(click)="togglePlay$.next()"
#gifPlayer
playsinline
preload="none"
[loop]="true"
[muted]="true"
[src]="src()"
></video>
</div>

We are now dynamically setting the background to use the thumbnail for that particular GIF.

Because the thumbnails are generally going to be a low resolution, we are also adding a blur to disguise that fact (instead it makes it look like a mysterious preview). However, we don’t always want to blur the thumbnail. There are some special thumbnails — like those that appear for NSFW (Not safe for work) videos — that we don’t want to display with a blur. That is why we use a dynamic class which will allow us to dynamically apply the styling depending on the loading status and whether we are dealing with one of our “special” thumbnails.

Keep in mind that if you want the special thumbnails to display — like the NSFW warning — you will need to copy the assets from the assets folder in the source code provided with this module, or supply your own images.

The resulting styles in the application are still not great, but it is a lot better now and we can interact with the videos more easily. We will continue to improve the styling of the application as we go.

Take a break, have some fun playing with the gifs we have loaded in so far. Over the next few lessons we are going to crank up the complexity even more by handling things like loading in additional pages of gifs, allowing the user to change the subreddit whenever they want, and handling tricky situations like errors and what to do if we don’t get enough valid gifs returned from a single request.