Skip to main content

Taskmaster - Todo Webapp

·802 words·4 mins
Stas Fetisov
Author
Stas Fetisov
Aspiring Software Architect and Tech Enthusiast

What is Taskmaster?
#

Taskmaster is a todo-list webapp. Before starting this project, I knew I wanted to try out some new tools, so I decided to choose something simple at its core, so I could focus my effort towards learning the tools rather than creating complicated data models or figuring out ideas. A todo list app was perfect for that, as it is conceptually simple and something I was familiar with.

UI

Tech Stack
#

So what did I learn?
#

Relational Database Modelling
#

This project gave me a wonderful opportunity to design and reason a relational schema from the ground up. I had to plan tables for users and tasks while making sure relationships were normalised and queries were performant. I used an open-source Supabase client (which I later had the pleasure of contributing to!), which provided a nice query builder which allowed me to structure queries for common CRUD operations. One issue I ran into with this was having to refactor the schema to use OAuth UUIDs as primary keys for users rather than a regular numerical IDs, which caused a bit of trouble but taught me the importance of planning schemas ahead of time.

Relational diagram

CORS (Cross Origin Resource Sharing)
#

As every engineer does at some point, I ran into the issue of CORS blocking the communication between my frontend and backend. While it was quite annoying at first, learning about CORS turned into a very fun experience, since it perfectly matched my curiosity about application security. After doing some research, I implemented a CORS policy that made sure that legitimate requests were accepted while keeping the app secure. Overall, I learnt a very important skill for application security that will definitely be of use in future projects.

ginClient.Use(cors.New(cors.Config{
		AllowOrigins:     []string{"http://localhost:5173"},
		AllowMethods:     []string{"GET", "PUT", "POST", "DELETE", "OPTIONS"},
		AllowHeaders:     []string{"Origin", "Content-Type", "Authorization"},
		ExposeHeaders:    []string{"Content-Length"},
		AllowCredentials: true,
		MaxAge:           6 * time.Hour,
	}), sec.JWTValidatorMiddleware())   // JWT/OAuth2 middleware

Authentication with JWT and OAuth2
#

This was perhaps the most difficult and time-consuming part of this project, thought this also made finally getting it all the more rewarding. Prior to this project, I always loved the convenience of Sign In With Google buttons on websites, and I was aware of the security benefits of using an identity provider over a regular login system. This project was the perfect opportunity for me to learn how it worked and implement it myself.

My previous API projects were not complex enough to require authentication, so I could also learn about JWT authentication. After lots of issues and troubleshooting, I finally succeeded and the system worked as intended in conjunction with the other features, making this a production-ready app. This was definitely the most interesting part of the project.

Frontend Data Caching w/ Tanstack Query
#

After completing the backend, I had to figure out how to make a performant frontend. While it was quite easy to make some simple forms for interaction with the backend, it did not satisfy me. I did not like that every time a user wanted to see their tasks, they would have to wait for them to be fetched from the backend. While looking for a solution for this problem, I ran into a particularly intriguing npm package called Tanstack Query.

// returns tanstack mutator for task creation
export function useCreateTask() {
    const queryClient = useQueryClient()
    return useMutation({
        mutationFn: createTask,
        onMutate: async (newTask) => {
            await queryClient.cancelQueries({queryKey: ['tasks']})
            const previousTasks = queryClient.getQueryData(['tasks'])
            queryClient.setQueryData(['tasks'], (old: Task[]) => ([...old, newTask]))
            return { previousTasks }    // returns context object with the old tasks in case of error
        },
        onError: (_err, _newTask, context: any) => {
            queryClient.setQueryData(['tasks'], context.previousTasks)
        },
        onSettled: () => queryClient.invalidateQueries({queryKey: ['tasks']})
    })
}

This package was a tool perfect for solving my issue, as it let me create a cache on the frontend, and choose when to invalidate it or make updates. This minimised the load on the backend, but there was still an issue. When users wanted to make changes such as deleting tasks or creating new ones, there was a short period of time before the changes would reflect on the UI. After some brief investigating, I realised this was because the app was waiting for a response from the backend before showing the changes. To solve this, I implemented optimistic updates, which updated the UI preemptively, reverting it only in case of an error. This solved the lag in the UI, making it a far smoother user experience.

Conclusion
#

Overall, this was a very good project that taught me a lot that will certainly be helpful in future projects. If you wish to view the source code for this project, you may find it here: