Website Under Development
Next.jsStrapiHeadless CMS
Article written: 27 May 2025Development period: 08/2024 - 04/2025Reading time: 6 min

Virtual Library

This project, which began as the practical component of my bachelor's thesis, is my first proper attempt at making a full-stack application, drawing heavily on the principles of content management systems I grew familiar with during an internship. While the thesis itself delved into a specific analysis of headless CMS performance, here I'll focus on the broader architectural decisions, features, and key learnings. Source code is available on GitHub.

System Architecture

Based on the thesis goals, I had to design the application with a decoupled architecture. For the sake of clarity, I have organized the system diagram into three distinct layers: the Application Layer, the Content Management System (CMS), and the Data Layer. The diagram below illustrates the flow of information and the distinct responsibilities of each layer.

Technology Stack

The foundation of this project's technology stack was partially predetermined by the goals outlined in the thesis. As you can see from the diagram, for the application layer, I have used Next.js. I enjoy working with App Router and Server Actions paradigms. And as I have assumed, its support for ISR is a game-changer for CMS-driven sites. For the content management system, the idea to use Strapi came from an internship, where I have briefly worked with it when assisting with a project setup and migration.

Later in the project development, to address latency issues I have observed during deployment Strapi to Heroku, particularly on the book Browse page, I have retroactively integrated React Query. It made managing state easier and allowed for handling background data synchronization and optimistic updates. Lastly for styling, I used Tailwind CSS with shadcn/ui. This combination made it fairly easy for me to build a decent-looking and consistent UI.

Core Features

Here are some of the core features of the Virtual Library application:

  • User Accounts: The app uses JWT for authentication. I used Next.js middleware to protect routes, so only logged-in users can access their dashboards. Coincidentally, around the time I was presenting this project, a critical vulnerability was discovered in Next.js middleware that temporarily rendered my authentication implementation obsolete.

  • User Dashboard: Once logged in, users can manage their profile, including changing their profile picture, where Strapi handles the image uploads and optimization. They can also manage here their "favorites" list, allowing users to bookmark books and track stats of these books.

  • Book Catalog: Users can browse a catalog of books with details like author, genre, and description. The interface includes pagination and sorting options. Each book has its own page with more details and user comments.

  • Dynamic Content: The landing page, like many other parts of the application, is fully managed through Strapi. This means an admin can update or completely change the site's landing page content without needing to touch the code.

Implementation

Auth

A notable challenge was authentication. Due to the complexity of validating Strapi's JWT tokens directly in Next.js middleware, I implemented a custom authentication bridge using dedicated API routes and then issuing a separate, session-based token that the middleware could securely manage.

Deployment

The deployment journey was a tough lesson in infrastructure. I initially chose to use Heroku for the Strapi instance, but its lack of native monorepo support and noticeable latency issues prompted me to search for an alternative. After a brief and challenging stint with Strapi Cloud, which proved cost-prohibitive for a personal project, I discovered Railway. Its seamless Git integration, generous free tier, and overall developer experience felt like a generational leap, making the final deployment smooth and efficient.

Caching

I severely underestimated and misunderstood caching. Given that caching was a major focus of my thesis and underwent significant changes during development, my attempts to optimize performance through caching became quite the learning experience.

My previous understanding of caching was rather linear, I thought of it as a single layer or a cog in the application. However, in this project, I realized that caching is way more complex of a topic and requires careful thought. I ended up implementing three different layers of caching throughout the application.

Next.js's server-side cache speeds up page delivery by storing rendered pages, React Query's client-side cache ensures fast and seamless data updates within the app, and standard browser caching reduces redundant network requests for static assets. Together, these layers created a comprehensive caching strategy, though coordinating them proved more challenging than I initially anticipated.

What I Learned and Would Do Differently

Initially, I managed all state with React's useState and useContext. As you can imagine, this became complex. Introducing React Query was a life saver, and I'd use it from the start on a similar project. Even in my limited experience it simplified server state management significantly.

I didn't prioritize testing in this project, which I regret. In future projects, I would like to implement unit tests for critical components and integration tests for API endpoints to ensure reliability. In retrospect I would have definitely setup some unit test even if it was just for the dopamine hit of passing them. Also since my bachelor's thesis focused heavily on the performance of headless CMSs. While interesting, this led me to over-engineer some performance aspects that weren't critical for the core application. And the results weren't exactly meaningful.

Perhaps the most significant lesson was to think ahead, and to diagram ideas and architecture before starting to code. I didn't do that here (for the most part), and it led to some messy code and architectural decisions that I had to refactor later.

Lastly, a Footnote

Overall, I have found building the Virtual Library to be a very enjoyable and formative experience. Thanks to it, I was able to combine my understanding of different parts of web development I have learned in the past two years, into a complete application. It also unveiled my fundamental lack of understanding of certain areas in web development and strengthened for myself the importance of planning and architecture in software development.