paint-brush
How We Built This: A Platform for Crowdsourced Design Patternsby@patternite
130 reads

How We Built This: A Platform for Crowdsourced Design Patterns

by PatterniteJanuary 26th, 2021
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Built with Next, Apollo and MongoDB and deployed with GitHub Actions and Docker.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - How We Built This: A Platform for Crowdsourced Design Patterns
Patternite HackerNoon profile picture

The Design Pattern

Design patterns are code templates that can be applied to solve common problems. Object-oriented folks tend to talk about design patterns more than the typical programmer, yet everyone uses patterns in some form, whether they know it or not. If you're familiar with React, for example, fetching data after a component has rendered can be stripped down to a few necessary lines of code:

import React from "react"
import MyLoadingIndicator from "path/to/MyLoadingIndicator"
import MyDataRenderingComponent from "path/to/MyDataRenderingComponent"

const MyComponent = () => {

  const [data, setData] = React.useState(null)
  const [isLoading, setIsLoading] = React.useState(true)

  React.useEffect(() => {

    // we need to specify a separate, async function which we call in
    // this hook, since `useEffect` can't be async
    const fetchData = async () => {
      const response = await fetch("https://urlToFetchFrom.com")
      const responseData = await response.json()
      setData(responseData)
      setIsLoading(false)
    }

    fetchData()
  }, [
    // empty array indicates `useEffect` will run once after render
  ])

  if (isLoading) return <MyLoadingIndicator />
  return <MyDataRenderingComponent data={data}/>
}

This pattern can be reused anywhere client-side data fetching and rendering is required.

The Problem

Design patterns are spread all across the web, in books, in lecture notes - you name it. Wouldn't it be great if there was a single resource that housed these patterns, so that they could be pulled off-the-shelf and used immediately in an application?

"Off-the-shelf" is a critical element here. Rolling out a user authentication system, for example, is generally challenging and prone to security risks, especially if you've never done something like it before. An established user authentication pattern, however, could be grabbed and used quickly, with fewer risks involved. Not only that, but a good pattern resource would allow the wider community to evaluate such a user authentication pattern, identifying flaws, sub-optimal design, and security risks.

So we've arrived at a problem in software development: often times, optimal implementations are hard to find. Ideally, we could build a library of crowdsourced design patterns, which facilitates rapid pattern creation, access, reading and evaluation. Good, established patterns would exist under one roof. With this in mind, we set out to build our solution, called Patternite.

The Plan

We wanted Patternite to have a few basic features:

  • Users accounts
  • A design pattern editor
  • A design pattern viewer with comment section
  • A feed of patterns, which can be searched, sorted and filtered

These features would constitute a minimum viable product. Anyone would be able to view the design patterns on the site, and users who sign up for an account would be able to create, vote on, and comment on patterns. There would be a feed on the home page, showing the most recently created patterns. This feed could be filtered to show the highest-scoring patterns within a time-frame (day, week, month, millennium), or filtered by programming language. Additionally, all patterns in the Patternite database could be accessed with a basic text search.

The Implementation

We used the following stack:

  • MongoDB - Schemaless, great cloud service, and Mongoose is a fantastic ORM
  • Express - Tried and tested, not much else to say!
  • Apollo - We decided to try out GraphQL rather than a REST-based API, just for fun
  • NextJS - We wanted to ensure good SEO, plus a nice developer experience

The most challenging conceptual aspect of this project was designing the correct MongoDB data model for the job. Ultimately, this boiled down to a few considerations:

  • Users would read patterns far more than they would engage with them
  • Users, patterns and comments could be accessed separately, whereas a vote was intrinsically linked to the pattern it was cast on, and the user that cast it

You can see a simplified version of the data model we ended up using below. The lines indicate which object the given ID field is referring to.

We created three objects: a

User
object, a
Pattern
object, and a
Comment
object. To utilize the power our noSQL Mongo database, we went for a denormalization strategy to help optimize our database transactions.

First, since votes would never need to be accessed outside the user or pattern they exist on, we opted to embed them inside the

User
and
Pattern
objects rather than create a separate
Vote
object and reference it. This way, whenever a
User
or
Pattern
is read from the database, there is no need to perform a separate query to get its votes. The tradeoff here is that writes are more complicated: whenever a user votes on a pattern, both the corresponding user and pattern document need to be updated. This is okay however, since, as mentioned above, writes in this context are far less frequent than reads.

In addition to this , we embedded comments in the

Pattern
object, and referenced comments in the
User
object. Again, since we wanted fast reads for patterns, embedding the comments made sense. We have a separate
Comment
object which is referenced by the
User
, just in case we ever want to easily grab a
User
's comments in the future. Similar to embedding votes in patterns, embedding comments comes with its own set of complications when it comes to writing (not least of which is that each comment can also have its own votes - not shown in the above diagram). Nevertheless, this is an acceptable price to pay to ensure fast, uncomplicated reads.

On the server side, things were relatively straightforward. We used Express for the base server and to handle user authentication. Apollo was used to handle all requests to the GraphQL API. In retrospect, GraphQL saved us a lot of time implementing the API, since we didn't need to write an endpoint for multiple variations on the same data. For example, we wanted the home page to have a pattern feed, which would display a list of pattern cards. Each card would consist of the pattern name, description, language, creation date, and score. This is a subset of the fields a user would see upon navigating to the actual pattern page. In a traditional REST API, we would either have to implement two endpoints - one for the card, and one for the entire pattern - or, we could (over)fetch the entire pattern, just to display the card. With GraphQL, we could simply ask the API for the data we wanted - one global endpoint, no over-fetching data.

Finally, we used NextJS for the frontend server. Effective search engine optimization (SEO) was an important requirement for us, so client-side rendering solutions like Create React App (and the design pattern at the beginning of this article!) were not good options. Although web browsers are good at client-side rendering, it seems that Google’s new Core Web Vitals - quantifiable metrics that contribute to your site's SEO - are improved through the use of server side frameworks like NextJS. For example, NextJS significantly reduces two Core Web Vital metrics: Largest Contentful Paint (web page loading performance), and Cumulative Layout Shift (visual stability). This is because NextJS fetches data and renders HTML on the server before the webpage touches the browser. All the browser has to handle is some minor JavaScript injection. This contrasts with traditional client-side rendered React, where the browser has to build the HTML DOM (causing unstable visual shifting), and wait for data from external API requests (creating code blocking bottlenecks).

One other small-but-awesome feature of NextJS was the intuitive routing solution. Rather than having to rely on an external dependency like React Router, we could simply organize Patternite's pages into directories to naturally mirror the routing we wanted.

The Deployment

Below you can see a diagram showing the services we used and how they're connected.

First, due to the inherent difficulties of setting up database monitoring, replicas and automatic backups, we opted to use the MongoDB Atlas cloud service, which handles these by default. We went with a server-based architecture, running the frontend and backend inside Docker containers within a DigitalOcean droplet. Using Docker was painful (to put things lightly) but it allowed us to containerize the frontend and backend services - ensuring they were self contained and portable, should we ever choose to switch to a different cloud service or move to an orchestration tool like Kubernetes. We used NGINX as our reverse proxy, and Cloudflare for caching static assets.

For continuous deployment, we used GitHub Actions to build Docker images of the frontend and backend, and push them to Dockerhub whenever the

development
or
master
branches were updated. We used Watchtower to listen for changes to the images, pull them, and fire up new containers. We built a staging server based off the
development
branch, so we could test new features in a production environment before merging them with
master
, which would update the production server. Finally, the client is linked to Plausible (a privacy-oriented analytics platform), giving us basic information on which pages users are visiting on the site.

The Result

And so, we built a platform for crowdsourced design patterns. You can see the React client-side data fetching and rendering pattern from the beginning of the article below (or see it for real, here).

Overall, Patternite was a fun project, and we're continually working to improve it. The next steps are to implement a notification system, and server-side caching with Redis in order to reduce server load.

If you have an interesting design pattern that you've used in your own work, we encourage you to contribute it to Patternite! We'd also be grateful for any feedback or questions you might have - checkout the contact page to get in touch. We also make short, to-the-point YouTube videos on various patterns in Patternite, which you can find here.