It started with an idea: To revolutionize an entire industry. And with a hard deadline: The people we did this project for already booked a slot at a fair where they would present their idea. So, to be honest, we didn't develop the entire product. The actual logic that would revolutionize the industry was already in place as an external service. But it needed an engaging, explanatory landing page. A landing page that would convince people to use the product. A landing page that would allow customers to buy the product as easy as 1-2-3.

And that's where Liip came in. We needed to be quick and agile on this project. Four weeks isn't much to implement an entire payment process, some communication with an external service, and have a fully-responsive, beautiful and accessible website. So we got started.

Choosing the tech

One thing was for sure: We needed a backend. The payment provider we chose had a pretty straightforward process: Generate a transaction ID, redirect the user to the payment page with the said transaction ID, call a webhook once the payment is done and redirect the user to a friendly thank-you page.

A diagram describing the above process.

We needed a backend that we could run on an existing shared host. Unfortunately, the shared host the client has chosen didn't offer NodeJS in any way, so we were not able to simply use the SSR of any modern JS framework. Bummer. So, it was either PHP or Python, with Python as our favourite. An entire Django setup seemed too large, so we settled on FastAPI: Small, feels like express, fast and reliable.

The frontend application, however, could be written with whatever framework we liked, as long as we built it before deployment. That wouldn't be much of an issue thanks to Liip's continuous integration setup: Instead of copying the source to the server and building it there, we would compile it on a CI runner and copy the created files over.

We wanted this app to be modern from the very start. We wanted TypeScript support, extendability and blazing fast performance. We were also up for a little experiment. Neither of us had touched Vue3 before, and we settled on Nuxt3 with TypeScript: Even though it was only a release candidate, all the features we needed were stable and reliable. Besides: We would keep up with the versions anyways, so once the final release is published, we would be able to update quickly. Nuxt's static site generation would ensure high performance, too.

As the CSS framework, we chose Tailwind. A little opinionated, but not too much, easily configurable. With Tailwind's components and tree-shaking mechanism, we would squeeze out even the last bit of performance from this fantastic piece of software.

So far, so good. We had a plan, so, ready to go!

The process of getting things done

We parallelized a lot from the first minute on. We started with the UX and the design of the general look & feel of the page right away and tackled the DevOps setup in the meanwhile. Since we knew what tech we would use, the basic setup was done in no time. Next, we implemented the general look & feel and the backend database, while the design for most of the static content was created. Afterwards, we implemented that, and so on. The below diagram shows how we've accomplished a very streamlined approach to this application.

A diagram describing the above process.

And we actually got it done on time for the fair! However, let's reflect on the tech stack.

The advantages of the tech stack

Vue3 and TypeScript arguably feel way more streamlined to work with than Vue2 and regular JavaScript. The setup script with Vue's Composition API has allowed us to create very dynamic and lean components. We don't need to rely on things like nuxt-property-decorator anymore to get TypeScript running. The more-or-less zero-config approach of Nuxt3 makes it simpler and less error-prone.

import { defineNuxtConfig } from 'nuxt'

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  target: 'static',
  ssr: true,
  css: [
    '~/assets/css/tailwind.css',
    '~/assets/css/fonts.css',
  ],
  build: {
    postcss: {
      postcssOptions: {
        plugins: {
          tailwindcss: {},
          autoprefixer: {},
        },
      },
    },
  },
  generate: {
    fallback: 'error404.html',
    routes: [
      '/',
      // ... add more routes here for static site generation
    ],
  },
})

About a third of the above config is for Tailwind; the rest is optional.

Speaking of which: Thanks to Tailwind, most simple components consist of a single <template> tag and some markup - often enough to give content structure and style. Tailwind allows us to quickly change the look of any component to compare what feels better. Tailwind's mobile-first approach forces us to use best practices from the very start. And with its tree-shaking combined with component classes, the resulting CSS is minimal. We were able to extract mostly typography-related classes into Tailwind components.

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .h1 {
    @apply font-bold text-3xl md:text-7xl;
  }

  .h2 {
    @apply text-xl md:text-3xl font-bold leading-tight pb-8;
  }

  .h3 {
    @apply text-lg md:text-2xl leading-normal pb-2 font-bold;
  }

  .p {
    @apply md:text-lg leading-normal pb-2;
  }

  .a {
    @apply text-brand-500 font-bold text-lg md:text-2xl flex items-center opacity-100 hover:opacity-60 ease-in-out duration-300;
  }

  .a-inline {
    @apply text-brand-500 font-bold md:text-lg opacity-100 hover:opacity-60 ease-in-out duration-300 break-all;
  }

  .btn {
    @apply w-full block text-white bg-black-900 hover:bg-black-700 ease-in-out duration-300 text-center md:text-2xl p-4 rounded-[20px] shadow-md;
  }
}

The resulting CSS is no larger than 17.6kb.

FastAPI is basically express without express. We used SQLAlchemy to define the ORM and repository classes, defined the necessary routers for each endpoint and could then get going with the service and payment integration. There's little overhead to FastAPI itself because it is unopinionated and has minimal config.

It is, first and foremost, a very flexible framework. For example, dependency injection allows for simpler testing. Swagger/OpenAPI doc is created automatically, and it has excellent type support.

The disadvantages

Let's start with the obvious: Nuxt3 isn't quite ready yet. A lot of documentation is still missing. Most packages, such as nuxt-i18n, have ports but are still a work in progress. The community is testing and still finding bugs. Some things are not defined yet, and some concepts got revamped to the degree of being unrecognizable. So there is a learning curve when coming from Nuxt2.

Tailwind itself is rather verbose. Its utility classes make it non-apparent what a given DOM element is actually doing and cloak its purpose. There is a way around this by using components, but these are meant to serve abstraction purposes, not making the code any cleaner. One doesn't need Tailwind if they build a ton of component classes anyways. Tailwind has a flat learning curve initially but is rather challenging to master.

FastAPI's flexibility comes with drawbacks as well. The minimal nature of the framework makes it necessary for the developers to implement features from scratch they would get out of the box with Django. A good example is the middleware system: FastAPI doesn't allow to specify middleware per route; one has to work around that. The same applies to CORS: Due to the API being used by two players (the frontend and the payment provider), we needed separate CORS policies for each of them, which is not quickly done without extra effort.

Final thoughts

The tools we used for this fast-paced project have advantages and disadvantages, but in the end, they helped us significantly reduce the time to market for the client. Their benefits, especially the velocity their DX has given us, were a crucial factor in achieving the feat of delivering a fully responsive, gorgeous website that serves its purpose: Explain, amaze and disrupt an industry.