The article was initially published at a friendly site 👉 Ship SaaS, one the best SaaS templates based on Next.js.
And you also might be interested in building a Programmatic SEO site with automated website screenshots using ScreenshotOne, Airtable, and Launchman, instead of writing code.
Before I start, I must admit that I have zero experience or don’t have experience at all with React, Tailwind CSS, and Prisma. I also don’t feel that I am profound in JavaScript. And this is excellent news because you will see this stack through the lens of a newcomer, not a passionate fan of these technologies.
Why Next.js, Tailwind CSS, and Prisma
Next.js
I picked up Next.js for testing because it is popular and has good tooling around it. Yes, popularity is a good factor for me. I am tired of searching for solutions to infrequent bugs for the “underground” framework I chose in the past.
I want to find one widespread stack with good developer experience that is performant and scalable. I don’t say Next.js satisfies my criteria, but it looks like it might. That’s why I am building an app to test it.
Tailwind CSS
I mostly used Twitter Bootstrap as a CSS framework, but Tailwind CSS has become quite popular over the last few years. And it proposes a different approach from what you do with the Bootstrap framework.
From the beginning, Twitter Bootstrap focused on providing many convenient components like buttons, sliders, tables, etc.
Tailwind CSS is a utility-first framework. It means that there are no components at all by default, and you decide how you want to style your page.
However, in recent times, I have seen a lot of convergence in both approaches. Twitter Bootstrap adds more and more utility classes, and you can buy Tailwind UI or take a look at daisyUI and Flowbite, which provide many components that you might want to use along with Tailwind CSS.
Prisma
There are many popular ORM libraries for Node.js: Prisma, Sequelize, TypeORM, and others.
I decided to try out Prisma first because I like the simplicity of their approach. I don’t need to create a model as in ORMs based on Active Record and Data Mapper patterns.
Prisma gives me a typed abstraction over the database, and I can decide how I want to use it. I can build models or use Prisma directly — it solely depends on my needs.
I picked up SQLite as a database for simplicity of development. But you can use MySQL, PostgreSQL, MongoDB, and many other DBs supported by Prisma.
Wrap up
Next.js, Tailwind CSS, and Prisma can be good starting choices for indie hackers, solo entrepreneurs, and small and enterprise teams.
Let’s write some code and taste this stack!
Meet “Landing Page Hunt”
Have you ever seen ProductHunt, BetaList, Tools for Creators, or CtrlAlt.CC?
We will build a small application — site directory that contains a list of landing pages with screenshots. An app user can like pages and submit their landing page.
That’s all the functions we are going to implement. I want to focus only on the app’s core functions; everything else is irrelevant. I will expand a bit on the topic of authentication and subscriptions at the end of the post.
The source code of the project is available on github.com/screenshotone/landing-page-hunt.
Quick start
Let’s create a Next.js app:
Then install Tailwind CSS:
We need to update our template paths in tailwind.config.js
to match the Next.js directory structure:
To enable support for the Tailwind CSS classes we need to import it in globals.css
:
Then we can run the app in the development mode and start coding:
Let’s quickly create a basic template and render pages:
Sign up to ScreenshotOne to get a screenshot API access key and to make it work, we need to create a .env.local
file and set a SCREENSHOTONE_ACCESS_KEY
environment variable:
Let’s examine the result:
A few key highlights:
-
Next.js is based on React but solves its most popular pains, so we can quickly build components without thinking about routing, server-side rendering, and even API. It is a fully-fledged full-stack framework.
-
I used the getStaticProps function to provide data for the index page at build time. It means that our main page is not dynamic. It is static and already rendered — thus it is blazingly fast.
-
I didn’t set up Puppeteer to take screenshots of the sites but decided to use a screenshot as a service API. It gives a bunch of free screenshots to render websites, so that’s enough to play with our project. Because taking screenshots of the site is not a core topic of the post, and using Puppeteer would be a pain in the ass.
Refactoring to use dynamic content
We started by using static site generation of Next.js, but we want to like pages, submit pages and get updated results. So, we need to refactor the code to use a more sophisticated approach for our needs.
I propose to do two things:
-
Extract the logic related to landing pages into a separate unit — a
Directory
class representing the landing page directory. It will help us migrate to an actual database instead of an in-memory cache later. -
And use getServerSideProps to render pages dynamically on each request.
Extracting the “business” logic
The Directory
class will encapsulate all the logic related to the landing page directory:
To speed up development, I store everything in memory and once the app is ready, we will simply migrate to use a real database.
getServerSideProps
Instead of using getStaticProps
, we now switch to use getServerSideProps
. It will allow us to render the page dynamically on each request. Look how simple it is:
Sending API requests with Next.js
Let’s see how easy it is to add API handlers on the backend and send API requests from the client side. We are going to implement the “like page” feature.
First, I put the logic in the lib/directory.js
:
Then we create an API handler pages/api/like.js
:
Next.js will handle all the routing issues for us automatically. So, let’s update the page component pages/index.js
to send an HTTP request to the like API handler:
The result is that we can click on the like button. Then an HTTP request is made to the API handler, likes property of the page updated, and we receive the result so we can re-render all the landing pages.
More pages and forms
We need an opportunity to submit a landing page. So, let’s develop the page. It is straightforward to add new pages with Next.js, and as I mentioned before, routing is done for us.
When I create pages/submit.js
, Next.js will handle all requests to the page under the path /submit
.
Let’s create a simple submit page:
I added confetti after the submission by using the react-confetti library:
On submit, we read the form data and send a request to a submit API handler (pages/api/submit.js
):
And the apparent logic for the submit function in the Directory
class:
That’s how simple it is to develop with Next.js. Do you feel it?
Switching to a SQL database — a case for Prisma
The core functionality of our app is ready and tested. Since it is encapsulated, we can easily switch to storing data in the permanent database, not in temporary memory.
But before that, let’s install and configure Prisma:
Let’s create a directory, prisma
, where we will store all the Prisma-related configuration and start with the main DB configuration and schema:
The schema file is written in PSL (Prisma Schema Language).
Then, let’s create a seed file prisma/seed.js
with landing pages:
To make it part of the migration process, we need to specify the seed in package.json
:
And after running migrations, including seeding:
We are ready to use the Prisma
client in our application. Let’s instantiate the Prisma client in lib/prisma.js
:
PrismaClient
is attached to the global
object in development to prevent exhausting your database connection limit.
And we are ready to refactor the Directory
class to entirely rely on Prisma
:
I chose SQLite for simplicity. I also extracted the link generation logic to a separate module.
Securing API keys for screenshot API
The rendered screenshots in the app expose the screenshot API access key. For the development stage, it was OK, but for deployment to production, we need to store the key securely.
ScreenshotOne API allows using signed links. It means that every exposed screenshot URL is publicly signed, and if you try to change any parameter and to reuse (steal) an API key, you need to compute a new signature, but you can’t without having a secret key.
In addition to that, you can force ScreenshotOne API to accept only signed requests.
That’s how I generated the signed links with ScreenshotOne API SDK:
Another solution would be to render screenshots and store screenshots on the backend site and share rendered images with the front end. Or use the “upload screenshots to S3” feature by ScreenshotOne and then serve files from a CDN.
Expanding the app to complete the SaaS solution
And we have arrived at the stage when we want to make a bit of money with our small, simple project. Imagine allowing accept payments for featuring landing pages. But users will need to sign up. And we need to make sure that voting is fair. There are a few things required to do it:
- Authentication;
- Billing and subscriptions;
- Transactional emails;
- Dashboard to manage the app;
- Cover the app with tests;
- Make sure that the site is SEO-friendly;
- Internalization if we want to go wide.
Next.js has many solutions you can combine to solve all problems mentioned above.
But I want to propose something interesting if your goal is to ship SaaS and not just play with code like I love to do. I would argue that starting with a SaaS template is better to save time and energy.
Nowadays, there are many SaaS solutions you can pick up to bootstrap your project quickly. Take a look at Ship SaaS — a SaaS template based on Next.js, for example, and the problems it solves for you. It literally can save you thousands of hours.
Instead of summary
As you might sense, I enjoyed it a lot to build with Next.js, Tailwind CSS, and Prisma. I am impatient to try them for any new idea I will have in the future. Many problems are already solved in these frameworks, and it is easy to use them.
In addition, there are a bunch of good SaaS templates I can use if I want to bootstrap a SaaS quicker.