Building a Twitter Client with NodeJS and Angular

Jeremy Wilken
Share

In this tutorial, we’re going to look at how to build a basic Twitter client with NodeJS, and an Angular application to display your home timeline of tweets. This is a rapid tour with things to consider while setting up your own Twitter client and Angular application.

First, we’re going to build a NodeJS server, which will handle communicating between the Twitter API and the Angular application. Then, we’ll build up the Angular application to display and interact with your Twitter timeline.

While you may be able to go through this tutorial without any prior NodeJS or Angular experience, I’ll assume some prior knowledge in the article.

Setting Up the Project

You’ll need a recent version of NodeJS set up on your machine. Then ensure you have the Angular CLI. The links provided give you a good place to start if you need help with either of those tasks.

The project source code can be found on GitHub. You can get it all running locally by either cloning it with Git or downloading the files from the repo’s GitHub archive.

git clone https://github.com/sitepoint-editors/twitter-angular-client

Once you have the files, from your terminal you’ll need to run npm install to get all of the dependencies installed. Then we can get to work!

Creating a Twitter Client in NodeJS

To access Twitter’s API, we need to register for a new “app”, which is essentially a way for Twitter to give us a set of credentials. These are unique for your application, so don’t share them publicly anywhere. You must, of course, have a Twitter account to access the data.

To start, go to https://apps.twitter.com/ and select Create New App. You can fill out the name, description, and website URL for your app. (You can use a fake URL for now. If you publish your app it should be your actual website.)

From there, you’ll see the new app page with your details. Go to the Keys and Access Tokens page, where you can see a button to Create my access token near the bottom. Click the button, and then you should see four values: Consumer Key (API Key), Consumer Secret (API Secret), Access Token, and Access Token Secret. We’ll use these in a moment, so be sure to keep this information handy.

Creating the Twitter Client in NodeJS

Now it’s time to dig into our NodeJS server, which will bridge the gap between Twitter’s API and the Angular app. In the project, you should see the server.js file, which you’ll need to open and tweak.

First, you’ll need to update the block that contains the credentials you received from the Twitter app earlier. You should copy those values into the block here. We’re using a Twitter package called Twit to help us connect to Twitter, though there are others available with various levels of functionality.

const client = new Twitter({
  consumer_key: 'CONSUMER_KEY',
  consumer_secret: 'CONSUMER_SECRET',
  access_token: 'ACCESS_TOKEN',
  access_token_secret: 'ACCESS_TOKEN_SECRET'
});

Now we should be able to connect to Twitter. We’re also using the popular ExpressJS to create and manage our server. Now that you have the credentials installed, you can run the server.

node server

Our next step is to make several routes that will handle the HTTP requests our Angular application will need to make to load the Twitter data. Our first route is to get the current user, and validate their credentials. The Access Token and Secret you provided are linked to your Twitter account, so you’ll be the authorized user in this case. When this route is called, it will call the Twitter account/verify_credentials endpoint and return an object containing your user data.

app.get('/api/user', (req, res) => {
  client.get('account/verify_credentials').then(user => {
    res.send(user)
  }).catch(error => {
    res.send(error);
  });
});

The next route we’ll create is to get your home timeline. It requests the statuses/home_timeline endpoint, and passes a few parameters to give us more of the data we need.

Due to rate limiting on the Twitter API, we’ve implemented a simple cache that will only request new data once a minute (which is the max rate before you receive errors). It basically keeps track of the last response and the time it was requested, only allowing new requests to Twitter to run after a minute. Rate limiting is a primary design consideration to have when building a Twitter app.

let cache = [];
let cacheAge = 0;

app.get('/api/home', (req, res) => {
  if (Date.now() - cacheAge > 60000) {
    cacheAge = Date.now();
    const params = { tweet_mode: 'extended', count: 200 };
    if (req.query.since) {
      params.since_id = req.query.since;
    }
    client
      .get(`statuses/home_timeline`, params)
      .then(timeline => {
        cache = timeline;
        res.send(timeline);
      })
      .catch(error => res.send(error));
  } else {
    res.send(cache);
  }
});

Finally, we create a set of routes to handle like/unlike and retweet/unretweet actions for a tweet. This will allow us not only to read data, but also take action. These will require that you’ve set the application Access Level to Read and write (in case you changed it in the Twitter app settings).

app.post('/api/favorite/:id', (req, res) => {
  const path = (req.body.state) ? 'create' : 'destroy';
  client
    .post(`favorites/${path}`, {id: req.params.id})
    .then(tweet => res.send(tweet))
    .catch(error => res.send(error));
});

app.post('/api/retweet/:id', (req, res) => {
  const path = (req.body.state) ? 'retweet' : 'unretweet';
  client
    .post(`statuses/retweet/${req.params.id}`)
    .then(tweet => res.send(tweet))
    .catch(error => res.send(error));
});

There are many Twitter APIs for engaging with Twitter data, but the fundamental rules remain the same. The only major issue here is we’ve hard-coded credentials to a single user, which you’d need in order to set up your own OAuth server (or use an existing one) to handle the authentication aspects, which you can learn more about on Twitter Authentication documentation.

Creating the Angular App

Now it’s time to turn our attention to the Angular application that uses the server we created. We’ll take a look at the key aspects of the application and how they work to create the final result. We’ve built this application using Clarity for the UI layer (it gives us many useful layout components), but otherwise everything is just Angular.

To run the Angular application, just run the following command and then open up http://localhost:4200:

ng serve

Inside of the application, we have a model at src/app/tweet.ts which contains the TypeScript interface that describes most of the properties of a tweet (some have been omitted). I believe it’s essential to describe your types properly for large-scale Angular applications as well as smaller ones, so this interface gives us the shape of a tweet.

Angular TwitterService

First, we’ll need a service that can make requests to our NodeJS server to get the latest tweets. In Angular, the HttpClient is the utility you use to make HTTP requests, so I’ve created an Angular service to encapsulate the logic for these calls. Open up src/app/twitter.service.ts and you’ll see the following code:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../environments/environment';
import { Tweet } from './tweet';

export interface TwitterResponse {
  data: any;
  resp: any;
}

@Injectable()
export class TwitterService {

  constructor(private http: HttpClient) { }

  user() {
    return this.http.get<TwitterResponse>(`${environment.api}/user`);
  }

  home(since?: string) {
    return this.http.get<TwitterResponse>(`${environment.api}/home?since=${since}`);
  }

  action(property: 'favorite'|'retweet', id: string, state: boolean) {
    return this.http.post<TwitterResponse>(`${environment.api}/${property}/${id}`, {state});
  }
}

This is a fairly basic service, which has methods to build a request for each API that we’ll support. The user method will return the current user (which will always be you). The home method will return the latest 200 tweets in your home timeline (or how ever many appeared since the last tweet specified). Finally, the action property handles making either a favorite or retweet call, by sending a boolean state value to toggle the status.

This service is generic, and each of these methods returns an Observable. If you want to learn more about them, you can read about Functional Reactive with RXJS, but the way they’re used here is similar to how a promise works. We’ll see how to use them in a moment.

Using the Angular TwitterService to load user

We’ll use the TwitterService in a few places, starting with loading the AppComponent. We’ll use it to load the user details (which appears in the top corner), and to load the list of tweets for the home page. Open up src/app/app.component.ts and you should see the following code:

import { Component , OnInit } from '@angular/core';
import { TwitterService } from './twitter.service';
import { Tweet } from './tweet';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  providers: [TwitterService]
})
export class AppComponent implements OnInit {
  user;

  constructor(private twitter: TwitterService) {}

  ngOnInit() {
    this.twitter.user().subscribe(user => this.user = user.data);
  }
}

The AppComponent does one main thing using our TwitterService. The ngOnInit method fires as soon as the component has initialized, and requests the user data. Here we’re using the Observable returned by the TwitterService.user method, and when we use subscribe it will trigger the actual HTTP request to fire. Once it’s returned, the callback function stores the user property, which is used to display content in the navbar. You can see the user property bindings in the component template below, such as user.profile_image_url_https:

<clr-main-container>
  <clr-header class="header-4">
    <div class="branding">
      <a class="nav-link">
        <div class="title">Twangular</div>
      </a>
    </div>
    <div class="header-actions" *ngIf="user">
      <a class="nav-link">
        <span class="nav-text">
          <img [src]="user.profile_image_url_https" class="avatar" />
          @{{user.screen_name}}
        </span>
      </a>
    </div>
  </clr-header>
  <div class="content-container">
    <main class="content-area">
      <app-tweets></app-tweets>
    </main>
  </div>
</clr-main-container>

Also, the use of <app-tweets></app-tweets> will insert the TweetsComponent, which handles the actual loading and display of tweets, so let’s take a look at it now.

Displaying the list of tweets

To help separate our logic, we actually have two components to display the list of tweets. The TweetsComponent manages the list of tweets and also handles making requests to our NodeJS service for liking or retweeting a tweet. Then the TweetComponent is used to display the actual tweet formatting and display. I always recommend trying to separate components into distinct roles, and in this case the TweetsComponent is in charge of handling data interaction, such as loading and retweeting, and the TweetComponent has no knowledge of loading data but only displays content. We’ll start by looking at the TweetsComponent, so below are the contents of src/app/tweets/tweets.component.ts:

import { Component, OnInit, Input, OnDestroy } from '@angular/core';
import { Tweet } from '../tweet';
import { TwitterService } from '../twitter.service';

@Component({
  selector: 'app-tweets',
  templateUrl: './tweets.component.html',
  styleUrls: ['./tweets.component.scss']
})
export class TweetsComponent implements OnInit, OnDestroy {
  inflight = false;
  tweets: Tweet[] = [];
  ids = [];
  timer;
  since = '';

  constructor(private twitter: TwitterService) {}

  ngOnInit() {
    this.getTweets();
    this.timer = setInterval(() => this.getTweets(), 61000);
  }

  ngOnDestroy() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  }

  getTweets() {
    this.twitter.home(this.since).subscribe(tweets => {
      tweets.data.reverse().forEach(tweet => {
        if (this.ids.indexOf(tweet.id_str) < 0) {
          this.ids.push(tweet.id_str);
          this.tweets.unshift(tweet);
        }
      });
      this.since = this.tweets[0].id_str;
      this.cleanUp();
    });
  }

  cleanUp() {
    if (this.tweets.length > 1000) {
      this.tweets.splice(1000);
      this.ids.splice(1000);
    }
  }

  action(action, index) {
    if (this.inflight) {
      return;
    }

    const stateKey = (action.property === 'favorite') ? 'favorited' : 'retweeted';
    const newState = !action.tweet[stateKey];

    this.inflight = true;
    this.twitter.action(action.property, action.tweet.id_str, newState).subscribe(tweet => {
      this.tweets[index][stateKey] = newState;
      this.tweets[index][action.property + '_count'] += (newState) ? 1 : -1;
      this.inflight = false;
    });
  }
}

This component takes the role of handling all of the loading and interacting with the list of tweets. In the ngOnInit method, we call the method to get the tweets, as well as set an interval that reloads the latest tweets every 61 seconds. Remember, there’s a rate limit on how many requests we can make, so this helps keep us under the limit. The ngOnDestroy method just unsets the timer when the component is removed, which is good practice to always do to prevent memory leaks.

Then we have the getTweets method, which uses the TwitterService to request the home timeline. It also passes a string that contains the last tweet ID that was received, so we can request only the tweets since that ID was created. When we subscribe, the request is made and the callback gives us the list of tweets. Since we want to show the most recent tweets first, we reverse the array and then push them onto the existing list of tweets, update the latest tweet ID reference, and then do some cleanup. If we have over 1000 items, we drop the remainder to help keep the memory consumption in check.

It’s important to note that we’re using the id_str property from the tweets. This is because JavaScript (and subsequently JSON) cannot accurately process numbers above 53 bits (or in other words, JavaScript cannot process extremely large numbers, see Snowflake IDs).

The action method will be used to handle calling the TwitterService to favorite or retweet a tweet. It takes the action (a favorite or retweet) and then toggles the state of the property. (If you previously retweeted, it would unretweet, for example). A tweet contains metadata about whether or not you’ve already favorited or retweeted, as well as the counts of how many favorites or retweets exist. Since your action of favoriting or retweeting changes that state, this method also updates the tweet values accordingly.

The template for the component can be found at src/app/tweets/tweets.component.html and is shown below. It’s fairly simple, as it iterates over a list of tweets, and displays an instance of the TweetComponent for each tweet. If the tweet is a retweet, it binds the retweeted status as well. Twitter adds a retweeted_status property with the original tweet’s data if it’s a retweet, and if it’s what we really want to display. Since we want to display the retweeted status, it actually replaces the actual tweet when it’s present.

<div class="tweets">
  <div class="card" *ngFor="let tweet of tweets; let i = index">
    <app-tweet *ngIf="tweet.retweeted_status" [tweet]="tweet.retweeted_status" [retweet]="tweet" (action)="action($event, i)"></app-tweet>
    <app-tweet *ngIf="!tweet.retweeted_status" [tweet]="tweet" (action)="action($event, i)"></app-tweet>
  </div>
</div>

The template shows the use of input and output bindings on the TweetComponent. The inputs [tweet] and [retweet] pass data into the TweetComponent, and the output (action) calls the action method on TweetsComponent when an action occurs (either a favorite or retweet action).

To see how the tweets are displayed, let’s move to the the TweetComponent, which binds a lot of data into a card component and can be found at src/app/tweet/tweet.component.html.

<div class="card-header">
  <img [src]="tweet.user.profile_image_url_https" class="avatar" /> {{tweet.user.name}} (@{{tweet.user.screen_name}})
  <span *ngIf="retweet" class="retweeted"><clr-icon shape="sync"></clr-icon> Retweeted by {{retweet.user.name}} (@{{retweet.user.screen_name}})</span>
  <div class="card-header-actions">
    <button type="button" class="btn btn-icon" [ngClass]="{'btn-success': tweet.favorited}" (click)="toggleAction('favorite')"><clr-icon shape="heart"></clr-icon> {{tweet.favorite_count}}</button>
    <button type="button" class="btn btn-icon" [ngClass]="{'btn-success': tweet.retweeted}" (click)="toggleAction('retweet')"><clr-icon shape="share"></clr-icon> {{tweet.retweet_count}}</button>
  </div>
</div>
<div class="card-block">
  <div class="card-img" *ngIf="hasPhoto(tweet)">
      <img [src]="tweet.entities?.media[0].media_url_https" (click)="media = true" />
  </div>
  <p class="card-text" [innerHTML]="tweet | tweet"></p>
</div>
<div class="card-footer" *ngIf="!retweet">
    {{tweet.created_at | amTimeAgo}}
    <clr-icon shape="minus"></clr-icon>
    {{tweet.created_at | date:'medium'}}
</div>
<div class="card-footer" *ngIf="retweet">
    {{retweet.created_at | amTimeAgo}}
    <clr-icon shape="minus"></clr-icon>
    {{retweet.created_at | date:'medium'}}
</div>
<clr-modal [(clrModalOpen)]="media" *ngIf="tweet.entities.media" clrModalSize="lg">
  <h3 class="modal-title"><img [src]="tweet.user.profile_image_url_https" class="avatar" /> {{tweet.user.name}} (@{{tweet.user.screen_name}})
    <span *ngIf="retweet" class="retweeted"><clr-icon shape="sync"></clr-icon> Retweeted by {{retweet.user.name}}</span></h3>
    <div class="modal-body">
      <img [src]="tweet.entities?.media[0].media_url_https" />
    </div>
    <div class="modal-footer" [innerHTML]="tweet | tweet"></div>
</clr-modal>

I’ll just point out a few key aspects of this template. First, the two buttons in the .card-header-actions element show the number of favorites and retweets. They also have an event binding (click)="toggleAction('favorite')" which calls a method on click to handle the actions. This method will emit an event up to TweetsComponent, which is using the (action) event binding to capture.

Also, you can see a lot of interpolation bindings, which are the {{tweet.favorite_count}}. There’s a lot of content to display, so this is the easiest way to print text or content into the page.

Next, the main text of the tweet is bound directly to the innerHTML property of the .card-text element, as you see here. This is because we want to display HTML content instead of just text, because it allows us to inject content with links.

<p class="card-text" [innerHTML]="tweet | tweet"></p>

This binding to innerHTML is done because we have a custom pipe (which we’ll review in a moment) that parses the tweet and replaces some of the content with links. So for example, if a tweet has a URL in it, this will replace the plain text value with an actual anchor link. Similarly, if the tweet mentions another user, it does the same thing. We’ve also included the amTimeAgo pipes, which are a set of Angular pipes for time management.

Finally, there’s a clr-modal element at the bottom, which is a Clarity modal. If the tweet contains an image, and the user clicks on the image (found above in the .card-img element), it will open the modal with a larger version.

To wrap up this component, it’s useful to review the component controller in src/app/tweet/tweet.component.ts, which defines a few important attributes:

import { Component, EventEmitter, Output, Input } from '@angular/core';
import { Tweet } from '../tweet';

@Component({
  selector: 'app-tweet',
  templateUrl: './tweet.component.html',
  styleUrls: ['./tweet.component.scss']
})
export class TweetComponent {
  @Input() tweet: Tweet;
  @Input() retweet: Tweet;
  @Output() action = new EventEmitter<{property: string, tweet: Tweet}>();

  hasPhoto(tweet: Tweet) {
    if (tweet.entities.media
        && tweet.entities.media.length
        && tweet.entities.media[0].type === 'photo') {
      return true;
    }
    return false;
  }

  toggleAction(property: 'favorite'|'retweet') {
    this.action.emit({property, tweet: this.tweet});
  }
}

The component declares two inputs, @Input() tweet and @Input() retweet, and one output, @Output() action. The two inputs allow us to bind in the tweet to display, and if it’s a retweet we also bind in that tweet information. You saw these values being passed from the TweetsComponent template.

The output alerts the parent component when something occurs, and in this case we want to alert about an action to favorite or retweet the tweet when those buttons are clicked. This information is simply passed along, like a normal JavaScript event, and the TweetsComponent component will handle what to do with it through the action method.

Before we wrap up the way we display our tweets, let’s take a quick look at this TweetPipe, which we used to format and parse the tweet.

Using TweetPipe to format data

The last major feature to review is the TweetPipe, found at src/app/tweet.pipe.ts and displayed below. This handles the parsing of the tweet text and metadata to provide formatting:

import { Pipe, PipeTransform } from '@angular/core';
import { Tweet } from './tweet';
import { DomSanitizer } from '@angular/platform-browser';

@Pipe({
  name: 'tweet'
})
export class TweetPipe implements PipeTransform {

  constructor(private sanitizer: DomSanitizer) {}

  transform(tweet: Tweet, args?: any): any {
    let text = this.sanitizer.sanitize(tweet.full_text);

    if (tweet.entities.user_mentions) {
      tweet.entities.user_mentions.forEach(mention => {
        text = text.replace(new RegExp(`@${mention.screen_name}`, 'gi'), `<a href="https://twitter.com/${mention.screen_name}" target="_blank">@${mention.screen_name}</a>`);
      });
    }

    if (tweet.entities.urls) {
      tweet.entities.urls.forEach(url => {
        text = text.replace(url.url, `<a href="${url.url}" target="_blank">${url.display_url}</a>`);
      });
    }

    if (tweet.entities.media) {
      tweet.entities.media.forEach(url => {
        text = text.replace(url.url, '');
      });
    }

    text = text.replace(/\n/gm, '<br />');
    return this.sanitizer.bypassSecurityTrustHtml(text);
  }
}

When you create a custom pipe, you have to implement the transform method and return the value you wish to display. In this case, we receive the entire tweet object (not just the text, because we need the metadata), and process it in several ways. Twitter returns data in a consistent structure, so we just inspect each property to figure out if any URLs, media, or mentions are present. If they are, we replace those values with a link, or in the case of media it’s removed because images are already displayed.

However, Angular normally prevents you from passing HTML and binding it into a template for security reasons. Angular allows you to bypass this and handle sanitizing input directly. The way we’ve solved it here is to sanitize the tweet text first, which will remove any potentially dangerous content (such as links with javascript: or script tags). Then we modify the text string to replace mentions and urls with link tags. Finally, we use the DomSanitizer.bypassSecurityTrustHtml method to bypass the security restrictions for the text to display. However, since we sanitized the text at the beginning, the content can be trusted.

When you have a pipe like this, be very careful of security and I recommend reviewing the Angular security guide.

Summary

That wraps up our rapid tour of an Angular Twitter client, and we saw a lot of Angular’s key features on display, and learned how to build a basic NodeJS server that connects to the Twitter API. This is intended to be a basic example, but many additional capabilities could be added fairly easily, such as composing tweets, viewing user profiles, and other interactions. I encourage you to look into the Twitter API documentation to see what options you have and see what else you can build!