IONICWEBCREATOR
Engineering Journal
Architecture

How We Structure Modern Full-Stack Applications

A tour of the architecture we reach for by default — a typed monorepo, a service seam between UI and data, Server Components doing the heavy lifting, and one source of truth from the schema to the screen.

Jakir Hussian

Jakir Hussian · Founder & Engineering Lead

Jun 2, 202611 min read

Share

Updated Jun 5, 2026

There is no single correct architecture, but there is a default worth defending — one that gets out of the way on small projects and holds up on large ones. This is the shape we reach for on most builds, and the reasoning behind each part of it. None of it is exotic. The value is in how the pieces fit, and in the boundaries between them.

One repository, clear packages

We keep the whole system in one typed monorepo — the site, the admin, the API, and the shared packages that sit under all of them. A single git root means one place to reason about the system, atomic changes across the stack, and shared types that cannot drift because everyone imports the same source.

repository layouttext
apps/
  site/     — the public website (Next.js, mostly Server Components)
  admin/    — the internal CMS + Business OS surfaces
  api/      — the service layer and seed scripts
packages/
  types/    — canonical domain types (the single source of truth)
  ui/       — the shared component library and design tokens
  validation/ — runtime schemas that mirror the types
  database/ — the Prisma client and schema

The service seam

The most important boundary in the whole architecture is the one between the interface and the data. Pages never fetch from a database or call an API directly. They call a service — a small, typed function whose signature is a promise the rest of the app depends on and whose body is free to change.

app/(marketing)/engineering-journal/page.tsxtsx
// The page composes; the service resolves. The page has no idea whether
// getArticles() reads a mock array, a cache, or a live API — and it never will.
export default async function JournalPage() {
  const [featured, articles, categories] = await Promise.all([
    getFeaturedArticle(),
    getArticles(),
    getJournalCategories(),
  ]);
  return <JournalExplorer featured={featured} articles={articles} categories={categories} />;
}

This is what lets us build the interface before the backend exists, and swap a mock for a real API later without touching a single component. It is the same discipline that made this very Engineering Journal renderable as static data long before it was wired to the content spine behind it.

Server Components do the heavy lifting

The cheapest kilobyte of JavaScript is the one never shipped to the browser. So we render on the server by default and add client interactivity only where a user actually touches the page — a filter, a search box, a reading-progress bar. Everything else ships as HTML: fast, indexable, and cheap to run.

  • The listing is a Server Component; only the filter and search are client islands.
  • The article body renders on the server; the table of contents is the only interactive piece.
  • Data fetching happens once, on the server, close to the source — never waterfalled from the browser.

One source of truth, end to end

A single type describes a row in Postgres, the API payload, and the props of the component that renders it. We define it once in the shared @iwc/types package and let inference carry it the rest of the way. When a column changes, the type changes, and every consumer that no longer agrees lights up in the editor before it ever reaches production.

SeamWho owns the shape
Database → serverThe generated Prisma client
Server → clientThe shared @iwc/types package
Client → screenThe same shared type, narrowed with a utility

Runtime validation at the edges

Types vanish at runtime, so at every boundary where untrusted data crosses in — a form submission, an API response, a seed payload — we validate against a schema that mirrors the type. Inside the system we trust the types; at the edges we verify. That combination is what lets the interior stay clean without being naive about the outside world.

This is the same architecture underneath everything we build, from client sites to Pulse and Business OS. It is not the newest possible stack — it is the one whose boundaries we trust, which is a different and more valuable property.


  • #Architecture
  • #Monorepo
  • #Next.js
  • #TypeScript
  • #Prisma
Newsletter

New essays, in your inbox.

Occasional, considered writing on engineering products that scale. No cadence promises, no noise — only when we have something worth your time.

One or two emails a month. Unsubscribe anytime.

Building something that has to scale?

If the way we think about engineering matches the standard you're after, tell us what you're building. We'll be honest about fit and outline a concrete first step.