💻 Developer Challenge Solutions
Developer Week is well and truly completed. Join us today to see how one power user solved a week long project in just 15 minutes. Join our Discord server here: https://discord.com/invite/cloudflaredev
Hello, my name is Luke Edwards and I am from the Cloudflare Workers team. Today we are going to revisit the final bonus challenge from our Developer Week challenge series.
Now if you don't know what I'm talking about, the developer challenges were a week-long series of prompts that invited you to build something with the workers product lineup.
It all led up to this bonus finale which asked you to build a personal reminders application.
You could save, edit or complete your reminders and then your application should send you some sort of daily notification or a daily reminder about your reminders.
Gretchen and I walked through an example solution last week which was powered by text messages and made heavy use of Twilio workers and workers KV.
However, as with all the challenges, there is no one right answer.
So you could have taken this in any direction that felt right to you.
So today we have Charles Axel Paulos joining us to walk through his solution to the challenge.
Charles is the CTO and co -founder of Tappable and is an active member of our discord community.
So Charles, do you want to show us what you built and walk us through how you built it?
Yeah, so maybe the first thing that I can show is how workers to do as I called it works.
So if I just put in my email here, so I'm just going to do workers course at gmail.com.
So here what we have is I signed up. I created an account, like an account in this case, it's just an email.
I didn't build in any authentication here.
But you now have access to your to do's. So one of the first to do's I can do is, you know, show workers to workers to do's on Cloudflare TV.
And then just by pressing enter, it will just add this to my tasks, I can choose to complete it.
I can even undo this, or I can remove the task altogether with this nice little emoji.
I'll keep it here for now. And so let's see how I how I built this.
Yeah, looks great. So let's first have a look at the backend, the API. So here, I want to look at my worker, my Cloudflare worker.
It's not particularly long, just about 200 lines of code here. But what it does, it basically creates an API that I can interact with from the front end from the HTML files, which we'll look through in a second.
So I have four different routes that are basically set up with a little npm package called itty routers.
And in this case, I'm using itty router extras, which gives like a few extra features, which allows for easy, you know, returning of JSON or easy handling of 404 errors and that kind of stuff.
So I'm importing these. And then I'm also importing nano ID so I can generate unique IDs.
Now, one thing I did, and as you can see here, is that all of your data gets deleted seven days after the last edit, I didn't want to manually remove users' data.
So this is why I created a little constant called time to live here, which expires the entries after seven days.
So that's a nice little feature of KV store, which is where I'm storing all of the todos, is that you can set keys to automatically expire so that you don't have to think about that.
Very cool. Plus, if you don't get to reminder within seven days, you probably won't ever get to it, right?
Exactly. So, yeah. So here we have four routes, one, which is to get all of the todos, and it's based off an email param here.
One is to post a new todo. So that's the same API path, but a different method.
Then we have patch, which is to update them. So I can edit the text, I can edit the states of the todo.
So whether it's done or not, and then a delete, which allows me to remove a todo completely, like the little trash icon you can see here.
So I can also edit those and that will work too. Very cool. Do you want to show us what some of these routes are doing?
Yep. So let's just start with the very first one, the easiest one, which is here.
I'm just getting all the tasks from that email that we're getting from the parameter here.
And so I'm just awaiting todos.
Todos is a reference to the KV namespace that I'm using. I'm just getting the email.
So usually this will return some text, but you can pass in an optional parameter, an optional object here to coerce that text into a type.
So it returns some JSON directly.
And if this returns null, because for instance, I just create an account and I don't have any todos yet, I'm just returning an empty array.
So then I'm returning these tasks as JSON. Makes sense. Then we have creating a todo.
That is what we're here for in the end. So here I'm checking, I'm getting some content, which is posted to this endpoint.
And I'm checking if there's a task.
If there's no task, we don't really have anything to create. And I'm returning an error if a task is missing.
I'm also checking if the email that the user typed in actually matches an email, because later on we'll be sending emails and we don't want that to error out.
So if this does not match an email, we're also referring an error.
We're returning an error saying just like invalid email. The next step is just very similar to the first step.
It's just getting all of the existing tasks.
So here it's exactly the same step as before, just fetching the tasks from KVStore.
Then I create a new task. I'm generating a random ID with nano ID.
I'm also adding a timestamp because it's always quite handy to know when the task was created.
I'm not showing this in the front end, but at least it is saved in the backend.
The task is filled with the task content. And obviously, because you just created a task, I'm setting completed to false.
Now I am adding this to the top of the list of tasks that I just got from KV.
And I am writing all of that back into KV.
One thing to keep in mind here is that you have to write, you can only write text.
So here I'm stringifying the tasks so that text is written into KV. And here what you can see, this is completely optional, but just so that I don't have to delete user accounts, this is where you set up the expiry for the tasks.
So every time you write to your user account, it will set the expiration seven days later.
So if you're a frequent user of workers to -dos, then rest assured your data is not disappearing.
Yeah. So the third API route is simply a patch, which allows us to modify a task.
Here we're just making sure that if there's no user account, well, then we're throwing an error.
But for the rest, it is pretty straightforward with finding the index of the task that was passed through the body of the request.
And if we can't find it for an error, and then we just modify the task.
So very straightforward afterwards, we just also again write it and then we return that task back to the front end.
Yeah, makes sense. And then deleting is again, very similar.
We're just removing the task from the tasks array and then updating KV. So how does that look like in the dashboard in KV?
So this was before, so let me refresh this.
And now we can see that there is a key here in a second. There we go, which is the email that I typed in.
And then we have the task here as a text. And well, all of this is all nice, but we still need our worker to respond to an event, a fetch event.
And so here, this is basically where everything starts is once there is a fetch event that came into the worker, we respond with the router that handles that event.
This router here is itty router. Okay, cool. So it's accepting the request and all of these missing and JSON helper methods are just returning a response.
So this is what is really nice with itty router extras is that you can very simply just return errors and it will set the status.
So if I do return missing, so that was here.
Oh, I think I forgot to return here. So if I return missing, it will create a response, set a status to 404 and it will return the error message all in one very simple method here.
So this is what makes it very nice to work with.
And then I see in all of your other routes, not the get route, actually also the get route, you have with params and with content.
So I imagine this is itty router parsing out your email route parameter and pulling out the request body with that with content.
Yeah, exactly. So it's an express like syntax, right?
Like where you have the parameter here, I'm parsing this with params middleware and that will basically allow me to get the email out from here.
Nice. So what is another one?
So here I have an email and ID and then I can get email ID. If I pass in the with content middleware, then I will also access the content here, which is already parsed JSON from the request body.
So all of that I don't have to and it just makes it super simple and super clean to kind of create a worker.
Yeah, nice and standardized.
Exactly. So next up we have the front end. So in this case, I tried to keep it simple and I used some of the tools I'm already aware of.
And so I used Tailwind for the styles.
Tailwind UI is what I used for the visualization.
So that's a set of copy, pasteable components. So while it looks very verbose and very complex, it really is just some very simple HTML that I mostly copy pasted.
I did not write any of this by hand. It's mostly copy pasting. I mean, it looks good.
So that's what matters. But so here we're looking at the index, right?
So that is when we go back here. So this is this index page and this is really just a very simple HTML form.
So another interesting thing is that these HTML files are deployed on Cloudflare pages, which is brand new, just launched, is just recently available, but is a very quick and easy way to deploy a website, a static website, I might add.
As you might have seen, all of these APIs here don't have any calls handling.
So that means that this worker has to run on the same domain as where my website is hosted.
With Cloudflare pages, that becomes super easy because you can just set a worker to run on the same domain.
And so this is why I didn't have to handle any of the options calls and all that.
So you can definitely make it more complex, but a nice thing here is that you can have the worker on the same route as your static hosting.
So that makes all that quite simple. And yeah, and so here on the tasks page, which is this one here, right?
All of the interactivity is coming from AlpineJS, which is a very simple, like it's a script that you can just include in your head.
And the nice thing is that it allows you to write kind of like logic similar to React or Vue, but directly in the HTML itself.
So that's why there's no build system. There is no npm run build and then npm run whatever.
This is really just two HTML files. And as you can see here, when creating a new task, there is an event listener that is indicated by the add here, and it's listening to a key up.
And then there's a modifier that is specifying which key, which is the enter key.
And then this then calls two functions, which is create task.
And then it passes new task, which is tied to this input with X model.
So X model is basically what is going to tie that new task variable with the input.
So that is AlpineJS magic happening there. Right, right. So a little bit differently.
So X model is the name of a variable binding, right? And this binding is created dynamically by Alpine, and you are interacting through it without having to worry about scopes.
Right. Okay. Very cool. And the scope is here. I just took the entire HTML, which is defined by X data, which then calls data, which is just a function here in a script, which returns an array, an object, sorry, with a couple of properties, new task being one of them, and then also functions.
So this is similar-ish to Vue in a sense, but then without having to build.
And yeah, it's like Vue Lite, very light.
It's HTML enhanced, right? Exactly. Yeah. So let's check out and see what that create task is doing.
Yeah. So that create task here. So here, I'm just making sure that there is a task to create.
So if the task is an empty string, this will not do anything.
And then I'm just doing a fetch call to the API that I defined.
And because the worker and the hosting is on the same domain, I don't have to specify the domain here, just slash API slash to this.
And then I'm passing the email that was passed through the get, which is here.
Oh, I see. Yeah, great.
So very simply. Okay. So that looks like it's reading. So on the index page, you entered your email.
That was a form action to slash tasks. And because it was a form submit, your email exists in the URL bar.
Exactly. Okay. Very cool. So clever little way to do that.
Old school HTML there. Yeah, yeah, yeah. Yeah, that's great.
So yeah. So basically, then I'm stringifying my task and I'm setting the correct headers so that the worker will recognize this as JSON.
And just to, sorry, just to loop back.
So on the worker side, that's where the the with content idi router helper kicks in.
Does idi, suppose this wanted to be, you know, a form data instance.
Does idi router accept? Okay, cool. So I think it does. Yes.
So it basically, like, however you pass you, whatever data you throw at your routes, if you add the with content, it will try and pass that data and return it to you.
Nice. Yep. So it's quite some complexity that has been taken care of. Yeah.
So, and then we just, you know, get the response. We get the JSON, the data from the JSON, and then we just add this task that we created to the tasks array, which is defined right here.
Okay. Great. And you've unshifted it just like you did in the backend code.
Yeah. Great. So that way it mirrors the state of the backend. But if you reload the page, it will then, that's, this is the X init that I wanted to show also.
The X init is basically the method that will run when, when Alpine has initialized on your HTML.
And so in this case, I'm calling get tasks, which as you can see here, it's just fetching the, the tasks, the yeah, to do, sorry, I'm using to do's and tasks interchangeably.
It's all good. Did not want to refactor that once I figured out that I was using them, both of them.
So yeah. So here I'm just getting the to do's and then passing that as Jason and then setting the tasks here to be what is in the database.
Nice. So that this is, so this get tasks, you said it's on in it.
So I guess just to reiterate, this means that this HTML page is completely client side, right?
There's, there's no a task information embedded in the page in the pages website page.
It's just a static HTML file. Yeah. Very cool. So there, there would be a way for instance, to have the worker kind of fetch the data, write it into the HTML with the HTML rewriter.
Cool. Yeah. Can we look at the complete task method because edit task looks to be similar to.
So edit task and complete task are very similar in a sense, they are basically patching the, the task with the ID here.
And they're basically passing the element that needs changing. So if you edit the task, so that's just editing the text of the task that will then in a body pass the task through.
However, if you want to complete the task, we are patching that task with completed which can either be true or false, depending on what the user clicks on.
Nice. Yeah. And then delete task is looks to be the same.
It's just except that afterwards I'm not doing much with it. Like, yeah.
Oh yeah. And I'm, I'm first removing the task from, from my local array before calling it so that it updates the interface instantly instead of having to wait for the API call.
Not that with workers API calls take a lot of time. You just, yeah.
You just want the instant feedback. Yeah. Yeah. Exactly. Yeah. That makes sense.
So I want to make sure we cover everything. Can, how did you tackle the reminders, the reminders about your reminders aspect?
So with the reminders, I used a scheduled event.
So this is something that you can set in, in here, you can set a Chrome in the wrangler.toml.
So this is running every day at noon. And basically I'm calling the send emails function.
The send emails function is really just fetching all of the, all of the user accounts that emails from that are in the to-dos database, and then just making sure they are emails and sending out some emails with the tasks.
So in this case, I am fetching all the tasks like we did earlier a couple of times, and I'm just filtering them so that, you know, I'm taking out all of the completed tasks because you don't want to get an email about something that you already did.
Right. And if that array length in the end is zero, so if there are no tasks left to be done, then I'm just resolving this premise immediately and we don't have to send an email.
But if there are still emails, if there are still tasks that are open, we basically send that by email.
And then we are sending it through SendGrid, which is slightly more complex and not super exciting.
But yeah, basically it is sending an email from no reply at workers course to the user's email and just setting the content to be a text HTML email with as a value the HTML that we built right here.
Okay. Yeah, that's cool. So was there a particular reason why you opted for email versus Twitter?
Yeah, very simply, SMSs can be quite expensive depending on the country you're sending SMSs to, whereas emails are basically free.
And yeah, I just didn't want to spam people with text messages every day.
Yeah, that makes sense. And also sending SMSs to US phone numbers requires registering your phone number and buying an SMS enabled phone number and stuff like that.
And just too much stuff to do in an afternoon. I definitely understand that.
The last thing I see here. So it looks like to use SendGrid, there's that headers authorization.
So that SendGrid key, that's just a Wrangler secret, I imagine.
That is indeed a Wrangler secret. So that you can just write to Cloudflare through the Wrangler CLI or in the interface itself in the Cloudflare dashboard.
Cool. So we have about five minutes left. Is this code available for people to play with it?
So it's available on GitHub.com slash workers course slash workers to do.
And yeah, you can find the code here. Do you want to tell us about Tappable?
Are you working with workers with Tappable? I am working with workers.
So just to give you a little bit of an idea of what Tappable is.
So I'm the CTO of a company called Tappable. And what we do is we basically have a tool that allows you to create web stories, which are stories just like on Instagram, but that live on a webpage.
So here I'm on my desktop. But as you can imagine, it's obviously a lot more interesting on mobile.
So you can scan this QR code if you want and check it out on your phone.
But I will show it here on the desktop.
And just similar like on Instagram stories, you just tap to move forward.
You can have video content. And one of the cool things here is that we can also have polls and quizzes.
So in this case, I answered correctly. And you can see the percentage of people who answered correctly or incorrectly.
And a really cool thing is that this is completely built on Durable Objects, which is in beta right now, I think.
And so it gives you a real time feedback. So this updates instantly.
And yeah. So if you answer right now, the numbers might be different because I influenced this.
And the people on the stream might also influence this. Very cool.
So do you mind telling us about how this is actually set up? Like, is this a worker serving the data?
Is there some KV involvement? Yep. So the whole story, which is just an HTML file, is basically stored on KV and is delivered instantly to the user.
Same thing with the images are also resized in function of the device and the resolution.
And all of that happens with Cloudflare image resizing. So yeah. So the whole serving of the stories is based on Cloudflare's infrastructure, which allows us to kind of deliver these stories in a very short time.
So yeah. Like over the past more than 7 million story views that we've had, like the median time to completely loading the story, including video and all that, is 700 milliseconds, which is quite nice.
That's definitely impressive with a very video heavy experience.
So yeah. And then how about this interactive form itself? Is this part of that initial HTML response?
No, that's an API call that happens once the story is loaded.
But yes, it happens also pretty quickly. But you don't really need the results until the user taps on it.
So you're fine doing an API call later on.
Yeah, exactly. I think you had mentioned to me privately that when creating these experiences, you do some sort of like eager pushing of the data.
Was that correct?
Yeah. So we basically... So yeah, all of the creation of the stories generates an HTML file, which then we push to KVStore so that it's available everywhere in the world, really close to the end users who will be watching the stories.
So we've had these stories viewed from over 200 countries around the world. And so it's very handy to have that data already in a static form that can instantly be returned to the user from anywhere in the world so that we don't have to hit our infrastructure in the backend.
So yeah. And then if you update your story, it's available within seconds worldwide.
Nice. And that's all through the REST API?
That is all through the REST API. Yeah. Yeah. Very cool. So we have now 40 seconds.
Yeah. So the last thing I want to pitch is, so I've been working with workers for the past two years now.
And I understand that sometimes it's not like the same thing that you're used to using.
So that's why I'm building the Complete Workers course, which is a video course that I'm working on.
Pun intended. And so, yeah.
So if you want to sign up or have early access to this workers course, you can just leave your email on workerscourse .com and yeah.
Cool. Learn how to build experiences with workers.
Well, thank you, Charles, for joining us and welcome to your solution.
Thanks. And thank you everyone for watching. Bye bye. Thank you. Bye.