How I Sync Daily Challenge With Leetcode API to Todoist

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.

Even though most of us don’t have to invert a 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 (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. If you simply want to deploy and use the app on your own, check out the repository here.

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 as my main productivity tool of choice, I have a daily task that looks just like this:

My Todoist habitual tasks
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 questions 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 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 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.

Network inspection
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:

# 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:

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:

// 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

💡
DEPRECATED: This version of the REST API (v1) is deprecated and will be removed on 30 Nov 2022. Please refer to the v2 documentation.

Like what we did in the previous step, I was able to find the official API documentation for Todoist. Typically, the first section that I always look for in API documentation is the 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
  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 this:

curl --request POST \
  --url 'https://api.todoist.com/rest/v2/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:

const TODOIST_API_ENDPOINT = 'https://api.todoist.com/rest/v2'

// 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 try them.

It runs on V8 JavaScript, not Node.js

This is probably one of the most common misconceptions about Cloudflare Worker. 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.

Lucky for us, we only need to use the JavaScript built-in fetch API.

More code

Starting a Cloudflare Worker project is dead simple (reference), basically:

  1. Install the Wrangler CLI using npm install -g @cloudflare/wrangler
  2. Run wrangler generate <your-project-name> <worker-template>
  3. The entry point is addEventListener function. For our use case, we will be using the ScheduledEvent API where we simply have to change our event from "fetch" to "scheduled"

Let’s stitch everything together:

// 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:

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

...

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

With the setting above, the worker will run every day at 00:01 UTC and sync the Daily LeetCoding Challenge to your Todoist.

That’s all! Moving on to testing.

How to test Cloudflare workers locally

To try out the Cron triggers locally, we would need to install the Miniflare CLI. After installation, you may run your worker:

# 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 and use the “Deploy with Workers” 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 WorkerSettingsVariables. You can get your Todoist API token here.
  3. Optional: This is only required for Wrangler actions. Add CF_API_TOKEN to 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 the question tag

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

While there is a lot of hate for coding interviews, I 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 in 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. Thanks for reading!

Written by human. Hosted on Digital Ocean.