DEV Community

Nick Bull
Nick Bull

Posted on

Building Twitter Post Scheduler With React, NodeJS, and Fauna (Part 1)

Today we are going to build a Twitter Post Scheduler application. Let’s call it Twittler.

Tools

Before we start, make sure you have

  1. Node and NPM on your computer. You can download both at nodejs.org.
  2. A code editor. I prefer VSCode. You can download it at code.visualstudio.com.

Twittler High-Level Architecture

Here’s a quick overview of how our application will work.

Twittler High-Level Architecture

Client part:

  1. Users write a tweet, choose a time when they want to schedule it, and click send.
  2. A tweet goes into the database and is stored there.

Server part:

  1. NodeJS calls the database every minute to retrieve all the tweets that need to be sent out that minute.
  2. Then NodeJS posts these tweets on Twitter.

Twittler Tech Stack

To build Twittler, we will use:

  • NodeJS (on the server)
  • ReactJS (on the client)
  • Fauna (as our database)

But before we dig into the code, first, we need to create a Twitter developer account to get access to Twitter API to start posting on Twitter.

Twitter Developer Account

Here’s how to create a Twitter developer account:

  1. Go to https://developer.twitter.com/en/apply-for-access
  2. Click the “Apply for a developer account” button

    twitter developer account

  3. Choose why you want to get access to Twitter API (I chose to explore)

    twitter api

  4. Complete all of the following steps, answer questions and submit your application.

    Twitter questions

  5. Wait until the Twitter team finishes reviewing your application and gives you access to the API (It may not take more than a couple of days)

  6. After the Twitter team will approve your application, go to your dashboard and create a new project.

    twitter api dashboard

  7. Then copy and save a Bearer Token. We will need it to access the Twitter API.

    twitter api

Now, let’s set up our database.

Fauna

For the database, we will use Fauna. It is a serverless database that gives you ubiquitous, low latency access to app data without sacrificing data correctness.

  1. Log in or sign up at your Fauna account here.
  2. Click on “Create database.”

    fauna Create database

  3. Choose name and region

    fauna dashboard

  4. Click on “New collection.”

    fauna collection

  5. Choose collection name and click “Save.”

    faunadb collection

We just created a new database called "twittler" and our collection "tweets" where we're going to store our tweets.

What does “collection” mean for Fauna?

Collections are sets of data records, called documents. In our case, a set of tweets. If you are familiar with relational databases, collections are analogous to tables in them.

Now we need to generate API keys and put them in our application so that our server can access the database to retrieve tweets from it. Here’s how to do it:

  1. Go to the “Security” tab and click on “New Key.”

    fauna security

  2. Type a key name and click on “Save”

    fauna key

  3. Our API key was generated.

    fauna api

  4. Save the key somewhere. We will need it later to access Fauna from our application.

And the last thing we should do is create an index.

Indexes in Fauna allow us to retrieve documents by attributes other than their Reference. They act as a lookup table that improves the performance of finding documents. Instead of reading every document to find the one(s) you are interested in, you query an index to find those documents.We will use the Index to get all tweets from a specific date range.

To create it, go to “Indexes” tab:

fauna indexes

And create a new index, “tweetsByDate”

fauna new index

Click the “Save” button, and let’s start to code our client.

Client

To create our client application, we will use ReactJS, and we can quickly install it using create-react-app.

Open your terminal and install create-react-app by using the following command:

npx create-react-app twittler
Enter fullscreen mode Exit fullscreen mode

Then go to the created folder and initialize our project:

cd twittler && npm i
Enter fullscreen mode Exit fullscreen mode

Now, let’s install Fauna package that we will use in our application to get access to the database from our client:

npm i fauna
Enter fullscreen mode Exit fullscreen mode

And also, we need to add the Fauna secret key that we have created in the Fauna security tab. To do this crate .env.local file in the root folder of our project and put your Fauna secret key there:

// .env.local

REACT_APP_FAUNADB_SECRET=your-secret-key
Enter fullscreen mode Exit fullscreen mode

The last thing we need to add is TailwindCSS. It’s a utility-oriented CSS framework to help us quickly design and build our application without writing any CSS. To do it go to public/index.html and add a link to TailwindCSS css file.

// public/index.html

<html lang="en">
 <head>
   <meta charset="utf-8" />
   <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
   <meta name="viewport" content="width=device-width, initial-scale=1" />
   <meta name="theme-color" content="#000000" />
   <meta
     name="description"
     content="Web site created using create-react-app"
   />
   <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
   <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

   <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
   <title>Twittler</title>
 </head>
 <body>
   <noscript>You need to enable JavaScript to run this app.</noscript>
   <div id="root"></div>
 </body>
</html>
Enter fullscreen mode Exit fullscreen mode

This is not the best way to add TailwindCSS to applications, but we remove extra installation steps required when installing "correctly" with npm. In real-world applications, we would install Tailwind using npm.

Now that everything is set up, time to write some code. Let’s start with creating our UI.

twittler ui

Open src/App.js and add this code:

// src/App.js

import React, {useCallback, useState} from 'react'
import Fauna from 'Fauna'

const currentDate = new Date().toISOString().substr(0, 10)

const FaunaClient = new Fauna.Client({
 secret: process.env.REACT_APP_FAUNADB_SECRET,
})
const q = Fauna.query

function App() {
 const [tweet, setTweet] = useState('')
 const [date, setDate] = useState(currentDate)
 const [time, setTime] = useState(
   new Date().getHours() + ':' + new Date().getMinutes()
 )

 const sendTweet = useCallback(
   async (event) => {
     event.preventDefault()

     console.log(new Date(`${date} ${time}`).getTime())
     console.log(new Date(`${date} ${time}`))

     try {
       FaunaClient.query(
         q.Create(q.Collection('tweets'), {
           data: {
             tweet,
             date: new Date(`${date} ${time}`).getTime(),
           },
         })
       )

       setTweet('')
     } catch (error) {
       console.log(error)
     }
   },
   [date, time, tweet]
 )

 return (
   <form
     onSubmit={sendTweet}
     className="flex flex-col max-w-lg m-auto min-h-screen justify-center"
   >
     <h2 className="mb-6 text-center text-3xl font-extrabold text-gray-900">
       Your Tweet
     </h2>
     <textarea
       required
       maxLength="280"
       rows="5"
       className="mb-6 focus:ring-indigo-500 focus:border-indigo-500 border-2 w-full p-4 sm:text-sm border-gray-300 rounded-md"
       placeholder="I don't understand pineapple pizza"
       value={tweet}
       onChange={(event) => setTweet(event.target.value)}
     />
     <div className="flex items-center mb-8">
       <input
         required
         type="date"
         min={currentDate}
         value={date}
         onChange={(event) => setDate(event.target.value)}
         className="focus:ring-indigo-500 focus:border-indigo-500 border-2 w-full p-4 sm:text-sm border-gray-300 rounded-md mx-4"
       />
       <input
         required
         type="time"
         value={time}
         onChange={(event) => setTime(event.target.value)}
         className="focus:ring-indigo-500 focus:border-indigo-500 border-2 w-full p-4 sm:text-sm border-gray-300 rounded-md mx-4"
       />
     </div>
     <button
       type="submit"
       className="flex justify-center py-4 px-4 border border-transparent font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
     >
       Schedule Tweet
     </button>
   </form>
 )
}

export default App

Enter fullscreen mode Exit fullscreen mode

So what’s happening here?

Using Fauna.Client, we create a Fauna client function with a secret key as a parameter to access the Fauna API.

const FaunaClient = new Fauna.Client({

 secret: process.env.REACT_APP_FAUNADB_SECRET,

})
Enter fullscreen mode Exit fullscreen mode

Using FaunaClient.query, we send a request to Fauna to create a new document with tweet and date parameters.

       FaunaClient.query(

         q.Create(q.Collection('tweets'), {

           data: {

             tweet,

             date: new Date(`${date} ${time}`).getTime(),

           },

         })

       )
Enter fullscreen mode Exit fullscreen mode

To test this code yourself, open the terminal and type the following command:

npm start
Enter fullscreen mode Exit fullscreen mode

And try to write a tweet like “I don’t understand pineapple pizza” (tell the truth), pick a date, time and click on the “Schedule Tweet” button. If the request succeeds, then you have created a new document in the collection tweets.

You can view the result in your Fauna dashboard:

fauna dashboard

The client part is done, now let's write our server.

Server

Our server will live in the server folder in the root folder of our application. Create the folder and place a single file, index.js, out of which we'll run our server.

To create our server, we will use a popular NodeJS framework ExpressJS (we could only use NodeJS for our application, but with ExpressJS, we will create API routes and extend our functionality application in the future).

To install ExpressJS, run the following command in the root folder of your project in the terminal:

npm i express
Enter fullscreen mode Exit fullscreen mode

After, we need to install the cron-job package so that our server can request Fauna every minute. To do it run the following command in your terminal:

npm i node-cron
Enter fullscreen mode Exit fullscreen mode

Also, we need to install the dotenv package. It will load environment variables from a .env.local file into process.env. This way, we can access the REACT_APP_FAUNADB_SECRET variable from our server code.

To do it, run the following command in your terminal:

npm i dotenv
Enter fullscreen mode Exit fullscreen mode

The last package we need to install is the twitter-api-v2. It will help us to post tweets on Twitter.

npm i twitter-api-v2
Enter fullscreen mode Exit fullscreen mode

Before we start coding, we need to add the Twitter API bearer token we saved in the previous steps. To do this, open the file .env.local and add your Twitter bearer token under the REACT_APP_FAUNADB_SECRET:

// .env.local

REACT_APP_FAUNADB_SECRET=your-secret-key

TWITTER_BEARER_TOKEN=your-twitter-bearer-token
Enter fullscreen mode Exit fullscreen mode

Now, let’s write the server itself. Open server/index.js and add this server code:

// server/index.js

const express = require('express')
const cron = require('node-cron')
const Fauna = require('Fauna')
const {TwitterApi} = require('twitter-api-v2')

const twitterClient = new TwitterApi(process.env.TWITTER_BEARER_TOKEN)

const q = Fauna.query

const faunaClient = new Fauna.Client({
 secret: process.env.REACT_APP_FAUNADB_SECRET,
})

// run every minute
cron.schedule('* * * * *', async () => {
 const now = new Date()
 now.setSeconds(0)
 now.setMilliseconds(0)

 try {
   // get all tweets from Now - 1 minute to Now
   const {data} = await faunaClient.query(
     q.Map(
       q.Paginate(q.Match(q.Index('tweetsByDate'), now.getTime())),
       q.Lambda(['date', 'ref'], q.Get(q.Var('ref')))
     )
   )

   // post all tweets from date range on twitter
   data.forEach(async ({data: {tweet}}) => {
     try {
       console.log(tweet)
       await twitterClient.v1.tweet(tweet)
     } catch (error) {
       console.log(error)
     }
   })
 } catch (error) {
   console.log(error)
 }
})

const app = express()

app.listen(3001, async () => {
 console.log(`Server listening on ${3001}`)
})
Enter fullscreen mode Exit fullscreen mode

Let’s see what is interesting happening here.

The cron.schedule calls every minute the function responsible for publishing tweets to Twitter.

Using faunaClient, we get all tweets in the range of current time and a minute earlier.

   const {data} = await faunaClient.query(
     q.Map(
       q.Paginate(
         q.Range(
           q.Match(q.Index('tweetsByDate')),
           minuteAgo.toISOString(),
           now.toISOString()
         )
       ),
       q.Lambda(['date', 'ref'], q.Get(q.Var('ref')))
     )
   )
Enter fullscreen mode Exit fullscreen mode

And using twitterClient.v1.tweet(tweet) we post them on Twitter.

Our application is ready. Now, let's test everything.

Run Application

First, create a script in package.json file that will start our web server when we run the command npm start server in our console:

// package.json
...

"scripts": {
  "server": "node -r dotenv/config ./server/index.js dotenv_config_path=./.env.local",
  ...
}
Enter fullscreen mode Exit fullscreen mode

Then open a terminal and run npm start server in one window to start our web server, and in another window, run npm start to start our client.

The Twitter Post Scheduler app is ready!

You can find a repository with the final example here.

In the part 2 we will deploy our application to Vercel.

Discussion (0)