DEV Community

Cover image for Luminary: a NextJS migration story.
Awoyemi Abiola
Awoyemi Abiola

Posted on

Luminary: a NextJS migration story.

Introduction

We're back. After our four-week sprint that took Luminary from a blank repository to a fully functional community platform built in vanilla HTML, CSS, and JavaScript, we regrouped to migrate the entire project to a modern framework.

Luminary is a community-powered directory and news platform celebrating women who are driving change across all fields — business, science, arts, activism, sport, technology, agriculture, education, and more. It allows anyone to nominate or self-submit, is searchable by name and field, and surfaces daily and weekly news of women making impact globally.

For this two-week sprint, we moved Luminary off the vanilla stack and onto a proper application architecture. I would be writing about the migration in general, my role in it, and the lessons I picked up along the way.

The Migration

The old Luminary lived as a set of standalone HTML pages, which we had wired up with its own script and stylesheet. It worked fine, but as the project grows, sharing logic, keeping styles consistent, and integrating with the backend cleanly is foreseen to become hectic.

This sprint we restructured the project into a Turborepo monorepo with two apps — a Next.js (App Router) + TypeScript frontend and an Express + Supabase backend. On the frontend we adopted Tailwind for styling, Base UI for accessible primitives, TanStack Query for data fetching, and a shared design system of common components. The work was split across feature branches, with each member converting the pages they originally built. My share was the nominations flow and the directory and profile pages.

Week 1

Specific Features Implemented

This week I converted the nominations experience from the old DOM-driven form into the new Next.js stack;

  • Rebuilt the full nomination and self-submission form as composable React components.
  • Extended a shared form component library so every input shares a single source of truth.
  • Wired submissions through TanStack Query and a typed service layer.

Nomination Form Screenshot

Technical Implementation Breakdown

  • Shared form primitives: Extended the common form module into FormField, TextField, FormLabel, and a Base UI SelectField so labels and inputs are defined once and reused everywhere instead of being hand-written per form.
  • Data out of components: Moved form constants — initial state, tabs, and field options — into the data layer, keeping the components focused on rendering.
  • Design system consistency: Replaced raw <button> elements across the tabs, link fields, and collapsible sections with the custom Button component, and added a reusable Drawer wrapper around Base UI's drawer.
  • Service layer: Split the logic into a NominationService and a standalone UploadService, with a private toPayload method and a shared error utility for consistent messaging.
  • Submission flow: Swapped manual try/catch handling for TanStack Query's mutation onError, and conformed API errors to a typed response shape.

Challenges & Resolutions

  • Turning imperative code into components: The original form was built with direct DOM manipulation, so I had to rethink it as composable React components and lift the shared pieces — labels, inputs, button, drawer — into reusable primitives rather than porting the markup one-to-one.
  • Acting on review feedback: A good portion of the week was responding to PR reviews from Isaac, our repo maintainer overlord, tightening the conversion to match our conventions — using the design-system components, removing manual memoization since the project runs the React Compiler, and routing repeated logic through shared utilities.

What I Learned This Week

This week, from converting the nominations flow, I learnt how to think better and make cleaner use of component-driven, design-system-first way, how to model data fetching and submissions with TanStack Query, and how to structure a typed service layer in TypeScript that keeps API concerns out of the UI.

Week 2

This week shifted from forms to data, getting the directory and profile pages reading real records from the backend, and porting the last of my old pages across.

Specific Features Implemented

This week I migrated the directory profile page and connected them to the live API;

  • Ported the old profile page from the vanilla HTML/JS project into a Next.js dynamic route with live data.
  • Added a shared mapping layer between the API and the UI profile shape.

Profile Page Screenshot

Technical Implementation Breakdown

  • Profile route: Created /directory/[id] as an async server component and rebuilt the old profile.html sections — header, about, and related links — as components using next/image and the original decorative photo frame.
  • Shared mapper: Added a single toNomineeProfile mapper feeding both the directory list and a new getProfileById, normalizing inconsistent URL fields coming back from the API.
  • Image configuration: Allow-listed the remote image hosts in the Next.js config so profile and avatar images render through the image pipeline.

Challenges & Resolutions

  • Profiles returning "not found": The directory cards were keyed on the nominee id while the detail endpoint looks records up by nomination id, so links never resolved. I aligned both on the nomination id through the shared mapper.
  • The backend wouldn't boot locally: It was missing its environment file, and once that was added the Supabase client still threw — Node.js below version 22 has no native WebSocket, which the client requires, so I added a ws fallback to provide one.
  • A request black hole: Even with the server running, requests never reached it. Port 5000 turned out to be reserved at the OS level on my computer, so the kernel was answering instead of the app. Moving local dev to a free port fixed it instantly, interestingly this was an issue I experienced in the original project stack as well, but had to debug and resolve again as I had forgotten, higlighting the importance of documentation in projects, silly me.

What I Learned This Week

This week, from migrating the profile pages, I learnt new methods of integrating a typed frontend with a real API across server components, the value of a single mapping layer between API and UI shapes, and the importance of documentation across project sprints.

Conclusion

Two weeks in, Luminary has a more solid foundation — a typed, component-driven frontend, the hard part of the move is behind us. For now, at least, onto the next.

Github Repository

Luminary

Contributors

-Product Lead 1: Ramnan Ramyil
-Product Lead 2: Awoyemi Abiola
-People Manager: Emmanuel Dania
-Lead Maintainer: Isaac Shosanya
-Design Lead: Michael Omonedo
-Engineering Lead: Daniel Chisom
-Backend Lead: Isaac Shosanya

Top comments (0)