This article doesn't expect you to have any in-depth familiarity with AWS or IFTTT. However, it is required that you have accounts for both, and recommended that you've at least played around with both of them a little bit. The free tiers of both of these services is all that's required to get your personal API running, so this doesn't cost a single cent.
Disclaimer: This is not intended to replace a full-featured API and only works for your own TickTick account. It was built for a very simple purpose of mine, but depending on your use case, it could be perfect for you as well.
Anyway, let's get started! 🎉
Backstory
Over the past couple of months I've been experimenting with a little hardware project, the goal being to display a current to-do item I'm focusing on through an e-ink screen using a Raspberry Pi. On the program's startup, I was calling an API to grab my to-do list and store the items in an array, then cycling through each to display on my screen.
A problem came up when after the New Year I switched services to TickTick. I had been thinking about moving away from the last app that I used, and the fact that they have a built-in Pomodoro timer solidified the switch for me and I haven't looked back.
But, they don't have an open API.
It seems like it's an item that has been requested a few times, and currently seems to be an item that they're working on. But as of the writing of this article, they do not have one available to the public. I get it though, pushing new features to production takes time, but that was time that I didn't want to wait. So, I started digging around trying to find my own way.
I thought I'd just go the brute force method and use something like Puppeteer to sign in to the app and scrape out any data I could. But then I remembered IFTTT exists, and decided to see if they happened to have a trigger for tasks from TickTick. Lo and behold, they did! So my plan started coming together.
Here's how I did it, and how you can too.
Getting Started
First thing's first, if you haven't already, sign up for an Amazon AWS account. This is what will end up powering our actual API, using the following services:
- Lambda for processing incoming data from our API endpoints and sending it to/getting it from our database
- API Gateway for creating our endpoints and connecting them to our Lambda instances
- DynamoDB for being the NoSQL database storing our todo items
All of these services have generous free usage tiers that don't expire. Unless you're going to be using this API every minute, you shouldn't incur any direct costs.
Everything we need to do in order to set up these three services is going to be handled through the command line using the serverless
app. We'll be loosely following this tutorial to create our serverless AWS API.
Go ahead and open up a terminal or command prompt window, and run the following command to get the serverless app installed:
npm install serverless -g
Side Note: Don't have Node or npm installed? Check out this link and finish that before continuing with the above command.
Once that's completed, run this command to get the serverless todo demo API installed locally. This is going to be the basis for our API:
serverless install -u https://github.com/serverless/examples/tree/master/aws-node-rest-api-with-dynamodb -n ticktick-api
Navigate to the new folder created, ticktick-api
, and open up the code with your favorite editor.
Now it's time to start working on our API.
Creating our Serverless API
After opening up your new project, you should see a file structure that looks like this:
ticktick-api/├── todos/│ ├── create.js│ ├── delete.js│ ├── get.js│ ├── list.js│ └── update.js├── package.json├── README.md└── serverless.yml
It's a pretty simple layout, each one of the files under the todos
directory is a specific endpoint for our API. They're going to be powered by a Node Lambda instance, interact with our database, and return something back in the request. The serverless.yml
file ties everything together, and generates the AWS services that we need in this API.
Before we actually deploy this though, we'll need to make some slight adjustments based on the data that we're pulling in and storing.
Opening up create.js
, you'll see the following lines of code:
if (typeof data.text !== 'string') { console.error('Validation Failed'); callback(null, { statusCode: 400, headers: { 'Content-Type': 'text/plain' }, body: 'Couldn\'t create the todo item.', }); return;} const params = { TableName: process.env.DYNAMODB_TABLE, Item: { id: uuid.v1(), text: data.text, checked: false, createdAt: timestamp, updatedAt: timestamp, },};
This isn't bad, but I'd like to add the ability to store the list from TickTick as well as the todo item's text. So, let's make some adjustments to add that datapoint in there as well:
if (typeof data.text !== 'string' || typeof data.list !== 'string') { console.error('Validation Failed'); callback(null, { statusCode: 400, headers: { 'Content-Type': 'text/plain' }, body: 'Couldn\'t create the todo item.', }); return;} const params = { TableName: process.env.DYNAMODB_TABLE, Item: { id: uuid.v1(), text: data.text, list: data.list, checked: false, createdAt: timestamp, updatedAt: timestamp, },};
Okay, that's good. Let's open up our update.js
file as well and add that list attribute there too. I won't show the before here, but this is what lines 12-37 should look like with both the text and list attributes:
if (typeof data.text !== 'string' || typeof data.list !== 'string' || typeof data.checked !== 'boolean') { console.error('Validation Failed'); callback(null, { statusCode: 400, headers: { 'Content-Type': 'text/plain' }, body: 'Couldn\'t update the todo item.', }); return;} const params = { TableName: process.env.DYNAMODB_TABLE, Key: { id: event.pathParameters.id, }, ExpressionAttributeNames: { '#todo_text': 'text', '#todo_list': 'list', }, ExpressionAttributeValues: { ':text': data.text, ':list': data.list, ':checked': data.checked, ':updatedAt': timestamp, }, UpdateExpression: 'SET #todo_text = :text, #todo_list = :list, checked = :checked, updatedAt = :updatedAt', ReturnValues: 'ALL_NEW',};
Lastly, open up serverless.yml
and remove the frameworkVersion: ">=1.1.0 <2.0.0"
line entirely. Save that file, and we're ready to deploy.
Back in the command line, run the following commands to install any dependencies, bundle everything together, and deploy your API:
npm install serverless deploy
You should get back a set of endpoints like this:
Service Informationservice: ticktick-apistage: devregion: us-east-1api keys: Noneendpoints: POST - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/todos GET - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/todos GET - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/todos/{id} PUT - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/todos/{id} DELETE - https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/todos/{id}functions: ticktick-api-dev-update: arn:aws:lambda:us-east-1:12345:function:ticktick-api-dev-update ticktick-api-dev-get: arn:aws:lambda:us-east-1:12345:function:ticktick-api-dev-get ticktick-api-dev-list: arn:aws:lambda:us-east-1:12345:function:ticktick-api-dev-list ticktick-api-dev-create: arn:aws:lambda:us-east-1:12345:function:ticktick-api-dev-create ticktick-api-dev-delete: arn:aws:lambda:us-east-1:12345:function:ticktick-api-dev-delete
Note: If you get an error back about AWS provider credentials not being found, follow this guide to set them up in order to work with your serverless CLI app.
Alright, so now you should be able to visit your API endpoint at https://xxxxx.execute-api.us-east-1.amazonaws.com/dev/todos
, and as of right now it should be returning back an empty array.
If so, we're on the right track, and we can move on to getting data from TickTick into it.
Setting up IFTTT
Okay, let's head over to IFTTT and either create or sign in to your account. This is going to function as the tie between TickTick and our newly-created API.
Create a new Applet, and for the service select TickTick. You're presented with two possible triggers, of which we're going to use both. But for this first one, select New task created.
On the next screen, you'll determine if you want to limit this by only firing on select task lists, tags, or priorities. Me personally, I'm using the Inbox list, with all tags and any priority, so it looks like this:
Hit create, and we're brought back to the Applet creation screen for the TT in IFTTT.
Select the Webhooks service, and on the following page choose the only action available, Make a web request.
On this page, we're going to add in all of the details for the web request. The URL is going to be one of the ones returned to us by serverless earlier, and should end in just /dev/todos
.
Set the method to POST
, and the Content Type to application/json
.
Finally, for the body, we're going to create a JSON string that will also contain data from our TickTick to-do item, pulled in automatically with the help of IFTTT.
All of these form fields should look like the screenshot below:
Once that's saved, we're ready to test it out!
Head over to your TickTick app, and create a new to-do item. When making the trigger in IFTTT, if you set any of the optional requirements like a specific list or tag, be sure to set those to make sure everything is working correctly.
Within a minute or so, we should be able to load up the main /dev/todos
API endpoint in a browser and get back an array containing any of the items that we added to TickTick in our test. They each should contain the task name, list name, timestamps, and a unique ID. That ID can then be used to pull in a single item, update it, or delete it from the database altogether.
Marking an item complete
You may have noticed that the above only pulls in items to our API when they're created. But, what if we want to update an item in our API when it's checked off in TickTick?
We can do that! First head back to the IFTTT dashboard and create a new Applet, with TickTick as the service. This time, choose the other trigger, New completed task. Again, choose what filters you would like (lists, tags, etc), and moving onto the action step, select Webhooks.
We're going to make a request to an endpoint on our API called /dev/todos/complete
. Don't worry, we'll create this shortly. Use the PUT
Method, application/json
Content Type, the same Body from the create Applet, and save it.
Now, let's open our ticktick-api code back up, and take a look at the update.js
file. This normally would expect an ID in the URL along with data attributes posted to it, in order to update a todo item. However, we're going to do some slight modifications in order to determine what item from what list needs to be updated, and mark it of as checked in our DynamoDB instance.
This is what that updated file should look like:
'use strict'; const AWS = require('aws-sdk'); const dynamoDb = new AWS.DynamoDB.DocumentClient(); module.exports.update = (event, context, callback) => { const timestamp = new Date().getTime(); const data = JSON.parse(event.body); const params = { TableName: process.env.DYNAMODB_TABLE, ExpressionAttributeValues: { ':text': 'text', ':list': 'list', }, FilterExpression: 'list = :list and text = :text' }; dynamoDb.scan(params, function (error, result) { result.Items.forEach(function (element, index, array) { const updateParams = { TableName: process.env.DYNAMODB_TABLE, Key: { id: event.pathParameters.id, }, ExpressionAttributeNames: { '#todo_text': 'text', '#todo_list': 'list', }, ExpressionAttributeValues: { ':text': data.text, ':list': data.list, ':checked': true, ':updatedAt': timestamp, }, UpdateExpression: 'SET #todo_text = :text, #todo_list = :list, checked = :checked, updatedAt = :updatedAt', ReturnValues: 'ALL_NEW', } dynamoDb.update(updateParams, (error, result) => { const response = { statusCode: 200, body: JSON.stringify(result.Attributes), }; callback(null, response); }); }); });};
Now all we have to do is run serverless deploy
from our terminal again to update our Lambda instances and we're good to go! If we check off an item in TickTick, that same item in our API will have a checked: true
attribute.
That's it!
You now have your very own personal TickTick REST API that you can use to store items you've added in your lists and reference them programmatically in other applications. If you have any questions about this, please feel free to let me know in the comments.
Follow me here or on Twitter for more updates on what I'm using this API for, and other web development projects I'm working on.
My Newsletter
Read sampleSubscribe using the form below and about 1-2 times a month you'll receive an email containing helpful hints, new packages, and interesting articles I've found on PHP, JavaScript, Docker and more.