Skip to content

Instantly share code, notes, and snippets.

@smashercosmo
Last active February 26, 2024 02:19
Show Gist options
  • Save smashercosmo/f4fc1225865834ca4280a44dc433671d to your computer and use it in GitHub Desktop.
Save smashercosmo/f4fc1225865834ca4280a44dc433671d to your computer and use it in GitHub Desktop.
Welcome file

Frontend architecture for Tasks application

This document describes frontend architecture for a SaaS system to manage tasks. It covers infrastructure, tech stack, viable approaches to fulfill certain requirements and desirable API.

Overview of the architecture

Infrastructure

Vite bundler (which is basically an industry standard nowadays) is going to be used for bundling, code splitting and live reloading during development. Another two industry standards - ESLint and Prettier - are going to be used for linting and formatting correspondingly. We can also setup pre-commit hooks via Husky and lint-staged libraries to ensure that no linting, formatting or type errors slip through. The app will be written in TypeScript, which helps to reduce the number of runtime errors and also makes refactoring easier.

Framework

For this particular application, it makes sense to use React as a main framework. Not only because of its popularity and vast ecosystem, but because of its concurrent rendering capabilities and also upcoming offscreen rendering feature. As we might need to render a huge amount of data (we might not, but this is something to be discussed), we don't want to block user input (for example while filtering), when this data is being rendered or updated.

Code organization in general

If Tasks, Photos and Documents are separate applications, that use the same design system, it makes sense to put all the code (for apps, design system and maybe some common utils) in a monorepo. It will help to keep things consistent and in sync. And also we'll be able to share TypeScript, ESLint and Prettier configurations between packages. The most robust tool for monorepos nowadays is Turborepo, which heavily uses caching to instantly rebuild packages, no matter how huge a repository is.

Code organization in the app

There are two ways to organize code in the app: by type and by feature. While "by feature" approach looks attractive, my experience shows that it doesn't really work, because features often intersect, and you still need to extract common parts somewhere. I advocate for a hybrid approach: code is organized by type (components, pages, forms, charts and so on), but some building blocks are colocated. For example, some page component folder might also contain components (or types, or data fetching hooks, or stores) specific for this page.

API

If it's possible to write Tasks API using NodeJS, leveraging the existing services and databases, then I'd propose to use tRPC, a type-safe alternative to traditional REST, which allows sharing API types between frontend and backend. In this case, API package will also be in the monorepo. If it's not possible, then I'll just describe new endpoints later in this document.

State management

App state will be kept in four places: component's local state, global app state, server cache and URL. Global app state will be used for inter-component communication (if needed), server cache will contain all the fetched data and URL parameters will be used to persist certain app states (like current filter values, opened modal dialogs etc.) and share them between users or restore them in different browser windows.

Responsiveness

To ensure our application works well on all devices, we're going to use mobile-first approach, meaning that all the components and screens will be implemented for mobile displays first and then extended to other resolutions. We can also set up a testing pipeline which will create app screenshots for different resolutions on every commit. A hardcore approach would be to use something like AWS Device Farm to run our full-fledged e2e tests.

Simultaneous editing

For this feature, we're going to use CRDT technology. Later in this document, I'll propose some of the existing solutions.

Server rendering

I assume that SEO is not that important thing for the Tasks app, so there is no need for server rendering (which complicates the architecture) and we can just go with full SPA mode. We'll just have to be careful not to make user to download an extremely huge JavaScript bundle. The easiest and most straightforward solution to avoid that - "by page" code-splitting.

Technologies and libraries

  • Language - TypeScript
  • Bundler - Vite
  • UI framework - React
  • Routing - React Router (once again an industry standard). Another approach worth investigating is to use Remix framework (from the React Router creators), when they release SPA-only mode. It does a lot of heavylifting for you and comes with many performance optimizations out of the box.
  • Data fetching - React Query. Simply the best data fetching library, which makes it extremely easy to implement such common patterns as "infinite scrolling", "optimistic updates", "data persistence" and so on. By keeping all the data in React Query's cache, we'll be able to easily and instantly switch between different views for the same data without refetching.
  • Form management - React Hook Form. The most flexible and performant form management library.
  • Global state management - Jotai. Atom-based state management library with a small API surface. Single store solutions, like Redux, don't scale well, while atoms can be collocated with corresponding components and rerenders are automatically optimized.
  • Styling - CSS Modules + PostCSS. I think it's a perfect solution for styling. You write normal CSS and your styles are isolated. Other solutions like CSS-in-JS add too much overhead, come with learning curve and have performance implications.
  • Charts and graphs - Victory or VisX. Just two mature charting libraries from big players. Either can be picked.
  • CRDT - YJS, Tinybase, Liveblocks, PartyKit. We're going to need to compare these libraries and choose the most suitable solution for us.
  • Testing - React Testing Library (for unit tests) and Playwright (for e2e tests)
  • API - tRPC (if possible)
  • Repo management - Turborepo
  • Linting - ESLint
  • Formatting - Prettier

API enpoints

For all the task-related endpoints, I assume that every task has unique id

  • POST /tasks

    Endpoint to get all tasks. As we need to have complex filtering capabilities, this endpoint is a POST one. With GET endpoint, we might exceed the maximum length of the URL.

    Request body

    {
      filter: {
        query?: String // to search by title or full-text search 
        companyId?: String
        tags?: String[]
        assignee?: String
        ....
      },
      skip: Number // Depending on the use case we might want to have cursor based pagination
      limit: Number
      sortBy: String // field name
      sortDir: ASC | DESC
    } 

    Response

     {
       results: Object[] // array of tasks
       total: Number // total number of results fitting search criteria
     }
  • GET /tasks/:taskId

    Endpoint to get specific task

    Response

    Requested task

  • POST /tasks

    Endpoint for task creation.

    Request body

     {
       title: String
       type: String // type id
       tags: String[] // list of tags ids
       assignee: String // user id
       description: String
       companyId: String
       ...
     }

    Response

    Newly created task

  • PUT /tasks/:taskId

    Endpoint for task editing.

    Request body

     {
       title?: String
       type?: String // type id
       tags?: String[] // list of tags ids
       assignee?: String // user id
       description?: String
       ...
     }

    Response

    Edited task

  • DELETE /tasks/:taskId

    Endpoint for task deletion.

    Response

     {
       id: String // id of the removed task
     }
  • Other endpoints

    We're gonna need also the following endpoints in our application:

    • GET /companies
    • GET /companies/:companyId
    • GET /users
    • GET /users/:userId
    • GET /tags
    • GET /types

Known limitations of the design

Honestly I'm struggling to come up with any limitations except for inability to render all million tasks at once :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment