Table of Contents
Simple full-stack task management application. You can view, create and edit tasks.
Tasks are composed of:
- Name
- Description
- Due date
- Status
A task's status is based on its due date, with tasks due in the past Overdue, due within 7 days Due Soon and past 7 days Not Urgent.
This is based on the following user stories:
-
User should be able to create a new task, including the following fields:
- Name
- Description
- Due date
-
User should be able to view all tasks created in a list view, showing all the following details:
- Name
- Description
- Due date
- Create date
- Status
- Not urgent
- Due soon (Due date is within 7 days)
- Overdue
-
User should be able to edit task name, description, and due date.
-
If you simply want to run the application then you'll at the very least need Docker. Make sure you are able to run Docker in your CLI
docker -v > Docker version <your docker version>
-
If you would like to develop the app then it might be better to run the back end and front end separately. In that case you will need:
-
Clone the repo
git clone https://github.com/daeddy/task_organizer.git && cd task_organizer
-
(Optional, only for dev) Since only the backend needs to run in a Docker container you only need to install front-end dependencies
cd front_end && npm i
If you just want to run the app I have a root docker-compose.yaml config:
-
Build and run Docker containers (this will take 10-20s depending on your machine)
docker compose up --build
-
Open the application at http://localhost:3000
For development you will need to run the front end and backend separately.
The backend API will run in its own Docker container. I use Air for live-reloading, and it has been configured to work in the container, so there is no need to run the app directly.
# in task_organizer/back_end
docker compose up --buildThe API will be available at http://localhost:8080. You can view the schema and test the endpoints at http://localhost:8080/swagger/index.html
For the frontend app you can run the app using:
# in task_organizer/front_end
npm run devNote: This will only work if the backend API is running since I fetch the TS types from the swagger schema on build using swagger-typescript-api
The app will be available at http://localhost:3000
Cleanup (linting and prettify) can be done using:
# in task_organizer/front_end
npm run cleanupThis will be an overview of the thought process and the steps taken when building the app.
I started front-end development with some library sanboxing as it had been a while since I built a React app. I had seen some cool implementations with shadcn/ui and I wanted to give that a try and see if it made sense for this application. I bootstrapped the app using:
npm create vite@latest front-end -- --template react-tsand added tailwind v4.
After testing shadcn/ui I felt like it would cut down a lot of dev time and since shadcn/ui creates the components directly in the code (rather than installing a UI library). I can reduce my dependencies as well as extend or modify any imported components to suit my needs.
All the components under
src/components/uiare generated byshadcn/ui(with the exception oftaskStatusTag.tsx). I only needed to modifypagination.tsxsince the app would be a SPA and it used button links (switched them to be buttons instead).
I implemented the basic views using dummy data, shadcn/ui components, and react-router (library version) to show the tasks list and the individual task view.
Given the first 3 user stories (required on the assignment), I thought it would be best to build the API first since I would need the data types to have a single source of truth. I had worked on a GraphQL Go API before so I had an idea already of what I wanted to do:
- Generate API schema so the font-end can get data types
- Use an ORM like Gorm to interface with the DB layer
- Use an API library to configure and create the routes (Went with Gin)
- Dynamically generate the
Task.statusin one DB transaction. - Implement basic pagination for the
GET /tasksroute
I was confident that this would be the bare minimum to meet the requirements and be scalable.
For creating the API schema I used gin-swagger. This library generates a swagger API schema based on comment annotations. You can see them on the model.task & handlers.
Gorm took care of the main DB functionality (migrations, queries, etc..). Having worked with it before though, there may be times where we would need to override the functionality since many of Gorm's abstractions do not play well at scale.
Note: I implemented a delete function here though it was never part of the requirements.
For the status, I decided it needs to be a virtual field produced by the API, since potential sorting by status would not be possible if we did the calculation in the front-end. I used Gorm hooks (AfterCreate, and AfterSave), invoked after saving type queries. For Get /tasks:id I just invoke the calculation after the fetch since doens't have a big performance impact. For Get /tasks where there could be potentially 10s of 1000s of tasks, then it will be necessary to do the calculation in the query so that it all happens within the same DB transaction.
For pagination, I based it on this simple implementation I found (minus the sorting).
Note: If I were to implement sorting or filtering I would use a similar abstraction:
handlers.filteringand add the sort field to this.
To simplify the environment setup I used docker-compose with the API being built with Air to allow for live-reloading.
Once the API was running and I had a schema, I added swagger-typescript-api to generate the types. I configured it to run on predev and prebuild
// package.json
"predev": "npm run generate-types",
"prebuild": "npm run generate-types",This fetches the swagger schema and generates the types for the app.
With the configuration
// gen-types.ts
httpClientType: "axios",I was also able to generate an axios API client setup with all the request and data types.
Using the generated API client implementing API hooks was simple (see src/api/hooks) as all the API functions and the data types were now available. With that I began implementing them on the pages. I used react-hook-form for the save actions (create, edit) and implemented the API functions under a taskForm.tsx component that can do either action depending on if a task prop is passed to it. This form is used in the dialog component triggered by action buttons on each page (New task on TaskLists and Edit Task on task view).
Once the app was working as intended I implemented a root docker-compose so that the entire application could be run with minimal setup.
In order to make sure the API is serving before building the front end app (since it needs to fetch the swagger schema on build), I used a health check condition on the API:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/swagger/index.html"]
interval: 10s
timeout: 5s
retries: 5and made the front-end app depend on it:
depends_on:
backend:
condition: service_healthyNote: This has the side effect of causing repeating requests (on 10s intervals) to
GET /swagger/index.htmlwhile the API container is running.
With that the application can now be run full stack using one command!
As mentioned earlier on the pagination implementation part (see pagination implementation) if sorting and filtering were to be implemented I would use the same pattern.
For sorting I would add a sort field to the pagination handler that uses a sort interface. Then on the GetTasks function in the taskHandler I would use Gorm functions to apply the sorting to the query (like db.Order("...")).
Filtering (search) would need its own interface that can be extended to use specific fields on a struct. The implementation would be very similar to pagination.
This kind of abstraction would make it very easy to re-use this logic in any other potential handlers.
There were some big features that I simply didn't have enough time to implement:
Error handling, Caching and Testing
As you can see by the time I had finished I had already spent ~6 hours of dev time on this project. I felt that the main goal was to show the core functionality from the requirements, as such I decided from the start that these features would take a lower priority.
However, in the event that these would be needed I have a good idea of what it would look like:
In the front-end, I would use an error context that captures any raised errors. It would be wrapped around the application and potentially render an error page and/or trigger an alert (error variant) component that would also have its own context.
For the back-end, errors would be logged and use an error middleware.
Caching can be done on both the front-end and back-end.
For the back-end i would probably add a cache layer using redis. This is a very common solution that works well at scale (although with some memory costs). Alternatively I could also create a parallel read replica DB. With that I could offload read operations, and scale read capacity without affecting the performance of write operations on the primary database. I think I would need more discussions on this to see which solution is best for this platform.
For the client its not a high priority based on these user stories, but if high volumes of read requests are expected, then adding some caching solution on the API client would probably be needed. It would most likely use local storage.
For the front-end I would use jest as well as snapshot tests for unit testing. Back-end unit testing would be done mostly natively using Go's built-in testing package. I would probably also want to implement some kind of load testing solution to address the "high volumes of tasks created" risk.
Integration testing I would probably need some time to plan and see which tools would work best for this stack.