How I Sync Daily LeetCoding Challenge to Todoist
Tags: javascript, serverless, webdev, sideprojects
Published: 2021-12-31 06:00:48

If you have ever been on a job hunt for a software developer position, you would have come across the so-called LeetCode style interviews.

Despite the fact that most of us don’t have to [invert a Binary Tree](https://leetcode.com/problems/invert-binary-tree/) at our actual job, that is how most coding/technical interviews are conducted at big tech companies like Google and Microsoft. Yes, even at the likes of [unicorns](https://www.cbinsights.com/research-unicorn-companies) (except for Stripe, because they are cool) and startups.

In this post, I’ll be writing about the thought process of how I came about building and deploying a very simple JavaScript app for free with [Cloudflare Worker](https://workers.cloudflare.com/). If you simply want to deploy and use the app on your own, check out the repository [here](https://github.com/ngshiheng/todoleet).

## TL;DR

-   How to get the Daily LeetCoding Challenge question using LeetCode API
-   Creating a Todoist task using Todoist API
-   Schedule our app to run daily using Cloudflare Worker ScheduledEvent API
-   How to test a Cloudflare Worker Cron trigger locally with Miniflare

---

## Problem Statement

Since a year ago, I have been trying to make it a habit to solve the Daily LeetCoding Challenge (which I’m still struggling with). As I am using [Todoist](https://todoist.com/) as my main productivity tool of choice, I have a daily task that looks just like this:

![My Todoist habitual tasks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ybr1vh7bwp9ihw2ivbdr.png) 
> My Todoist habitual tasks

As a lazy person, having to check leetcode.com every time I want to practice is too much of a hassle. So then I thought, why not just sync the Daily LeetCoding Challenge to my Todoist every day?

## Requirements

Let’s start by defining what I want the app to do:

1. Get Daily LeetCoding Challenge question
2. Ability to create a new task on my Todoist account
3. Sync new Daily LeetCoding Challenge question on time
4. The app has to sync on time for free every day

Let’s start!

---

## LeetCode API

Like any sane person would do, the first thing I did was to do some research. By research, I meant I started to Google for information.

The first thing I did was to immediately Google for “leetcode API”, looking for the official API documentation.

### Official API documentation

To my surprise, there wasn’t any official LeetCode API documentation available. While there is a couple of unofficial LeetCode API repositories on GitHub, I would rather not use any unofficial API due to reliability concerns (poorly maintained, outdated, etc.).

### The Good Ol’ DevTool inspection

The second thing that immediately came to my mind was to inspect the network request being made while visiting the site https://leetcode.com/problemset/all/.

With this, I was able to figure out the exact API called to query for the Daily LeetCoding Challenge — done.

![Since LeetCode is using GraphQL, you would need to check out the “Payload” tab to see the GraphQL body](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kebw2s3s6y0kjugedpj6.png) 
> Since LeetCode is using GraphQL, you would need to check out the “Payload” tab to see the GraphQL body

Here’s the GraphQL request body:

```graphql
# HTTP POST to https://leetcode.com/graphql
query questionOfToday {
    activeDailyCodingChallengeQuestion {
        date
        userStatus
        link
        question {
            acRate
            difficulty
            freqBar
            frontendQuestionId: questionFrontendId
            isFavor
            paidOnly: isPaidOnly
            status
            title
            titleSlug
            hasVideoSolution
            hasSolution
            topicTags {
                name
                id
                slug
            }
        }
    }
}
```

You can use the `curl` command below to try it out yourself:

```sh
curl --request POST \
  --url https://leetcode.com/graphql \
  --header 'Content-Type: application/json' \
  --data '{"query":"query questionOfToday {\n\tactiveDailyCodingChallengeQuestion {\n\t\tdate\n\t\tuserStatus\n\t\tlink\n\t\tquestion {\n\t\t\tacRate\n\t\t\tdifficulty\n\t\t\tfreqBar\n\t\t\tfrontendQuestionId: questionFrontendId\n\t\t\tisFavor\n\t\t\tpaidOnly: isPaidOnly\n\t\t\tstatus\n\t\t\ttitle\n\t\t\ttitleSlug\n\t\t\thasVideoSolution\n\t\t\thasSolution\n\t\t\ttopicTags {\n\t\t\t\tname\n\t\t\t\tid\n\t\t\t\tslug\n\t\t\t}\n\t\t}\n\t}\n}\n","operationName":"questionOfToday"}'
```

Code
Enough talking, let’s start to write some code that does exactly what we went through:

```javascript
// Just some constants
const LEETCODE_API_ENDPOINT = 'https://leetcode.com/graphql'
const DAILY_CODING_CHALLENGE_QUERY = `
query questionOfToday {
	activeDailyCodingChallengeQuestion {
		date
		userStatus
		link
		question {
			acRate
			difficulty
			freqBar
			frontendQuestionId: questionFrontendId
			isFavor
			paidOnly: isPaidOnly
			status
			title
			titleSlug
			hasVideoSolution
			hasSolution
			topicTags {
				name
				id
				slug
			}
		}
	}
}`

// We can pass the JSON response as an object to our createTodoistTask later.
const fetchDailyCodingChallenge = async () => {
    console.log(`Fetching daily coding challenge from LeetCode API.`)

    const init = {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ query: DAILY_CODING_CHALLENGE_QUERY }),
    }

    const response = await fetch(LEETCODE_API_ENDPOINT, init)
    return response.json()
}
```

Task “Get Daily LeetCoding Challenge question” — checked.

## Todoist API

Like what we did in the previous step, I was able to find the [official API documentation](https://developer.todoist.com/rest/v1/#overview) for Todoist. Typically, the first section that I always look for in API documentation is the [Authorization](https://developer.todoist.com/rest/v1/#authorization) section, especially when you want to perform create/update/delete operations on an app.

In short, authorization was pretty straightforward for Todoist:

1. [Get your API token](https://todoist.com/app/settings/integrations)
2. Whenever you make a request, attach `Authorization: Bearer xxx-your-todoist-api-token-xxx` to your HTTP request header

Here’s an example of what the curl command to create a new task on Todoist would look like:

```sh
curl --request POST \
 --url 'https://api.todoist.com/rest/v1/tasks?=' \
 --header 'Authorization: Bearer xxx-your-todoist-api-token-xxx' \
 --header 'Content-Type: application/json' \
 --data '{
"content": "Buy a jar of peanut butter",
"due_string": "Today"
}'
```

### Code

Writing a function that does what we said is relatively easy, it looks something like this:

```javascript
const TODOIST_API_ENDPOINT = "https://api.todoist.com/rest/v1";

// Passing in the `question` object from fetchDailyCodingChallenge function
const createTodoistTask = async (question) => {
    const questionInfo = question.data.activeDailyCodingChallengeQuestion;

    const questionTitle = questionInfo.question.title;
    const questionDifficulty = questionInfo.question.difficulty;
    const questionLink = `https://leetcode.com${questionInfo.link}`;

    console.log(`Creating Todoist task with title ${questionTitle}.`);

    const body = {
        content: `[${questionTitle}](${questionLink})`,
        description: `Difficulty: ${questionDifficulty}`,
        due_string: "Today",
        priority: 4,
    };

    const init = {
        method: "POST",
        body: JSON.stringify(body),
        headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${TODOIST_API_TOKEN}`, // Set at environment variable
        },
    };

    const response = await fetch(`${TODOIST_API_ENDPOINT}/tasks`, init);
    return response.json();
};
```

Task “Create a new task on my Todoist account” — done.

## Cloudflare Worker

And we’re down to our one final task — running/automating the 2 tasks above every day, for free.

The first thing that came to my mind was Cron job. So, I immediately started looking for free solutions on the Internet. After spending a couple of hours doing some homework, I came across Cloudflare Worker, and I figured to give them a try.

### It runs on V8 JavaScript, not Node.js
This is probably one of the most common misconceptions [about Cloudflare Worker](https://blog.cloudflare.com/introducing-cloudflare-workers/). As the worker’s environment is not in Node.js, a lot of packages (e.g. `npm install some-node-package`) that are running on Node.js would simply not work.

> Tip: Check out the supported packages and libraries [here](https://workers.cloudflare.com/works).

Lucky for us, we only need to use the JavaScript built-in [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) API.

### More code

Starting a Cloudflare Worker project is dead simple ([reference](https://developers.cloudflare.com/workers/get-started/quickstarts)), basically:

1. Install the Wrangler CLI using `npm install -g @cloudflare/wrangler`
2. Run wrangler generate <your-project-name> [<worker-template>](https://developers.cloudflare.com/workers/get-started/quickstarts#templates)
3. The entry point is `addEventListener` function. For our use case, we will be using the [ScheduledEvent](https://developers.cloudflare.com/workers/runtime-apis/scheduled-event) API where we simply have to change our event from `"fetch"` to `"scheduled"`

Let’s stitch everything together:

```javascript
// Move the constants to const.js

const syncLeetCodeCodingChallenge = async (event) => {
    const question = await fetchDailyCodingChallenge();
    await createTodoistTask(question);
};

addEventListener("scheduled", (event) => {
    // Change 'fetch' to 'scheduled'
    event.waitUntil(syncLeetCodeCodingChallenge(event));
});
```

Next, we would simply need to modify the wrangler.toml as below:

```toml
name = "<your-project-name>"
type = "webpack"

...

[triggers]
crons = ["1 0 * * *"]
```

With the setting above, the worker will run every day at [00:01 UTC](https://crontab.guru/#1_0_*_*_*) and sync the Daily LeetCoding Challenge to your Todoist.

That’s all! Moving on to testing.

## How to test Cloudflare workers locally

In order to try out the Cron triggers locally, we would need to install the [Miniflare CLI](https://miniflare.dev/cli.html). After installation, you may run your worker using:

```sh
# At terminal 1
miniflare

# At terminal 2
curl "http://localhost:8787/.mf/scheduled"
```

If you see a new task is created on your Todoist, you have made it!

## Deployment

No side project is ever done without hosting it.

To deploy the app on your own immediately, check out the [project repository](https://github.com/ngshiheng/todoleet) and use the [“Deploy with Workers”](https://deploy.workers.cloudflare.com/?url=https://github.com/ngshiheng/todoleet) button. If you’re interested in the know-how:

1. Create a Cloudflare account.
2. Add `TODOIST_API_TOKEN` using `wrangler secret put TODOIST_API_TOKEN`. You may find the newly added secret under 'Cloudflare Worker' → 'Settings' → 'Variables'. You can get your Todoist API token [here](https://todoist.com/app/settings/integrations).
3. Optional: This is only required for [Wrangler actions](https://github.com/marketplace/actions/deploy-to-cloudflare-workers-with-wrangler). Add `CF_API_TOKEN` into your GitHub repository secrets. You can create your API token from https://dash.cloudflare.com/profile/api-tokens using the `Edit Cloudflare Workers` template.
4. Finally, to publish any new changes to your Cloudflare Worker, run `wrangler publish`

And we are finally done!

---

## Closing Thoughts

Finally, there’s a lot more that we could have done, e.g.:

-   Handling unanswered questions from previous days
-   Making the app configurable/customizable for users
-   Add tags to our task
-   Allowing users to create a random LeetCode question as a task based on question tag

I’m going to leave these features out for another day.

While there is a lot of hate on coding interviews as such, I personally look at it this way — by learning some brain teasers, you probably get paid a lot more, so why not? It is really a matter of perspective. If you happen to enjoy doing them, that is even better.

Personally, I don’t find as much joy doing LeetCode questions. Rather, I work on LeetCode problems as if I am lifting weights at the gym. While I don’t enjoy lifting weights, I do like reaping the benefits of it.

That’s all for today. Let me know what are you building with Cloudflare Worker!

---

_This article was originally published at [jerrynsh.com](https://jerrynsh.com/how-i-sync-daily-leetcoding-challenge-to-todoist/)_