Software architecture for non-technical builders
You don't need to be a mechanic to drive a car — but when the AI stalls, it helps to know what's under the hood.
I learned to code when I was nine years old. By some magical serendipity, my dad had just brought home an Apple //c computer and my mom had decided to take a programming class at a local community center. She never learned to code. She did, however, leave a thick block of dot-matrix printouts sitting next to a computer whose only use up until that point was playing Lemonade Stand.
(Years later, in another case of magical serendipity, I ended up working for J Allard — creator of that Lemonade Stand game and head of the initiative to launch the first Xbox.)
So other than justifying my gray hair, what’s my point? Though I never ended up writing code professionally, my technical experience has always been helpful as a product builder. It helped me bridge two worlds.
Today, like so many others, I’ve been experiencing a newfound joy in making software. For the first time, AI coding tools enable anyone to create working software without writing code. You don’t need to understand the underlying code to use those tools — but a basic grasp of software engineering will make you significantly better at it. You’ll provide sharper context, fix bugs faster, and build more ambitious things — whether that’s a prototype, an internal tool for your team, or a product you put in front of real customers.
I’m not saying you must become an engineer in order to be a great builder Think of it this way — you don’t need to be a mechanic to drive a car, but knowing a bit about how it works can help if your car ever breaks down. Similarly, it’s sometimes helpful to be able to peek under the hood of what you’re building so that you can point the AI in the right direction when something’s off. That alone can save you hours — especially when the AI is running in circles.
This is a lightning-fast overview of software architecture. We’ll cover the key concepts — frontend, backend, databases, and APIs — and how they fit together. Then we’ll open up the code from a real app and walk through what the AI actually generated.
💡 This piece is adapted from my AI Prototyping course on Reforge. The course is available on demand right now and we’re running the next live cohort this fall.
NOTE: This is over 8,000 words 😅. It’ll take some time to get through, but I promise it’s worth it if you’ve recently started building software.
Also, this is a work in progress. If I didn’t cover something you’re curious about or you have any questions, drop them in the comments below.
Grab a cup of coffee and let’s dig in.
How modern software works
The big picture
Most web apps, including the ones you’ll build, consist of four elements:
The frontend is what users see and interact with — buttons, layouts, images, animations. Note, the frontend can be a web app that runs in the browser or a native app that runs on iOS, Android, Mac, Windows, etc. For the rest of this piece we’ll primarily discuss how web-based frontends work.
The backend handles logic, workflows, and processing — the work that happens behind the scenes.
The database is where information is stored — user profiles, saved content, settings.
APIs are the connectors that let your app talk to external services — payment processors, mapping tools, AI models.
Although this is a simplification (especially for complex products), it is a useful mental model and can help us understand how the pieces fit together.
The frontend connects to the backend, and the backend handles all communication with the rest of the system — databases and APIs, which can include both internal services and external ones. This means the frontend doesn’t need to know how to talk to each of these individually; it just talks to the backend, which orchestrates everything behind the scenes.
The core mechanism for this is the request-response cycle:
The user types a URL or clicks a link
The browser sends a request to a server
The server processes the request — fetching data from a database, calling internal or external APIs, or some combination of all three
The server sends back code (HTML, CSS, and JavaScript)
The browser assembles that code into the page the user sees
When the user takes an action — submitting a form, clicking a button — the cycle repeats
With that mental model in place, let’s look at each layer.
The frontend: what users see and touch
The frontend is everything the user experiences — text, images, buttons, layout, animations, and more. It’s the user interface, rendered in the browser.
Three technologies work together to create it:
HTML is the structure — what appears on the page and in what order. A heading, a paragraph, an image, a link. Think of it as the skeleton of your interface.
CSS is the styling — how everything looks. Colors, spacing, fonts, borders, shadows. If HTML says “put a button here,” CSS says “make it blue, rounded, and 200 pixels wide.”
JavaScript is the behavior — what happens when users interact. Click a button and a modal opens. Type in a search field and results filter in real-time. Switch tabs and the content changes.
These technologies were designed when web pages were meant to be exactly that — hyperlinked pages in a vast network of documents. Modern web apps are far more than documents, and a whole ecosystem of tools has emerged to make it easier to build sophisticated apps on the web’s early building blocks. The most popular by far is React, a framework built by Meta.
Nearly every AI building tool — Magic Patterns, v0, Lovable, Bolt — generates code using React, and dev-focused tools like Claude Code, Codex, and Cursor reach for it constantly too. React organizes HTML, CSS, and JavaScript into components: reusable building blocks that you compose together like Lego bricks.
React components: the building blocks of your app
Let’s look at React components more closely with some examples.
A Button component defines what a button looks like and how it behaves. A Card component defines a container with a border and shadow. A Hero component combines an image, a title, and a call-to-action into the banner at the top of a page. Each component is self-contained, and you can use it as many times as you need. Change the Button component once, and every button in your app updates.
Components accept properties (”props”) — inputs that customize how they render. A Button component might accept a label prop (”Save” vs. “Cancel”) and a variant prop (”primary” for the main CTA on the page vs. “destructive” for a Delete button). The same component, different data, different appearance. This is why components are powerful: one piece of code can render dozens of different elements on screen.
When you tell your AI tool “make the save button green,” it knows to find the Button component and update a property to make the change. When you say “add a card for each activity,” it’s rendering the same Card component multiple times for each activity in the dataset. Understanding this pattern — components, props, reuse — makes your prompts significantly more precise and helps you understand how things are related.
This structure is one of the reasons a data-first approach to prompting works so well — when your data is well-structured, the AI breaks things into clean, well-defined components that map to the concepts in your app.
Tailwind CSS: why your code is full of class names
Traditionally, CSS styling lives in separate .css files — apart from the component’s structure and behavior. That works, but it means jumping between files to understand how something looks and how it works. Many developers prefer keeping styling right alongside the component code, in the same .tsx file. Tailwind CSS is a framework that makes this possible. Like React, it achieved widespread adoption and is now the default way AI building tools style the web apps they generate.
Open any file in an app generated by an AI tool and you’ll see something like this:
<h1 className="text-3xl font-bold text-white">Those class names — text-3xl, font-bold, text-white — are Tailwind CSS. Tailwind is a “utility-first CSS” framework where each class does exactly one thing. text-3xl sets the font size. font-bold makes it bold. text-white makes it white. p-4 adds padding. rounded-lg rounds the corners. bg-blue-500 sets a blue background.
Because the styling is right there in the class names, there’s no hidden stylesheet to decode. When you tell the AI “make the title larger,” it changes text-3xl to text-4xl. When you say “make the background darker,” it changes bg-gray-50 to bg-gray-100. The AI is doing simple string manipulation, and you can read the intent even if you’ve never written CSS.
You’ll start to notice patterns: text- controls typography, bg- controls backgrounds, p- and m- control padding and margins, flex and grid control layout. You don’t need to memorize these, but recognizing them will help you read your app’s code and make targeted requests.
You’ll also see responsive prefixes like sm:text-4xl or lg:px-8. These mean “apply this style only on small screens” or “apply this style only on large screens.” This is how your app adapts to different device sizes — a concept called responsive design.
Editing Tailwind classes is a great way to get started manipulating your app’s code directly. It’s fun to work with, gives you direct control over how things look, and it’s not prone to errors. If you’d like to go deeper with Tailwind, check out the Tailwind CSS documentation, which is extraordinarily well written.
For example, you can reference Tailwind classes in your prompts:
Change the hero section background from bg-white to bg-gray-50 and increase
the padding to p-8
The card titles are using text-lg. Make them text-xl font-semibold instead.
The button is using bg-blue-500. Change it to bg-emerald-600 to match our brand color.💡 Refer to the Tailwind Colors page for color names and a description of how to set the shade.
Pre-built React components (shadcn/ui)
Programmers love things that make their lives easier. React makes it easier to build UI components. Tailwind CSS makes it easier to make those components beautiful. shadcn/ui is another library you’ll encounter — a set of pre-built React components, so programmers don’t need to implement standard elements like buttons, accordions, or dropdown menus from scratch.
Most AI building tools default to shadcn/ui for common components. This is why your apps look polished out of the gate. The AI isn’t designing buttons from scratch — it’s reaching for battle-tested components and customizing them for your use case.
When you need a new UI component, it’s often helpful to refer to the library of shadcn/ui components. You can reference them in your prompts. For example, let’s say you want to add a group of related buttons. The ButtonGroup component does exactly this. You can even get a sense of the properties that are available by reviewing that component’s documentation page. Your prompt might look something like:
At the top of the UI, add a
ButtonGroup (<https://ui.shadcn.com/docs/components/radix/button-group>)
with buttons for New, Duplicate, and Delete.Web apps vs. native apps
Everything we’ve covered — React, Tailwind, shadcn/ui — pertains to web apps: applications that run in a browser. Native apps (iOS, Android) work differently, running directly on the device’s operating system rather than in a browser. Both have a frontend — in a web app, it’s what the browser renders; in a native app, it’s what the device displays natively.
For most people building with AI, web apps are ideal. They’re fast to build, easy to share, and work across devices — you can send a link to a stakeholder and they’re using what you built in seconds. This is one of the reasons AI building has taken off: the path from prompt to testable artifact is remarkably short.
The backend: what happens behind the scenes
Some apps are frontend only. Frontend-only apps run entirely in the browser (or on a device) and never communicate with a remote server. This works for simple tools like a calculator or alarm clock, and it’s perfectly fine for prototyping user experiences. Many prototyping tools, like Magic Patterns, focus entirely on frontend work.
But many features require a backend to work. Any time your app needs to do one of the following, there’s a server involved:
Storing data that persists across sessions or devices (e.g., saving a user’s profile or order history)
Handling authentication so the app knows who is using it (e.g., login, sign-up, session management)
Running sensitive logic that shouldn’t be exposed in the browser (e.g., pricing calculations, access control rules)
Connecting to third-party services through APIs (e.g., processing a payment via Stripe, generating text via OpenAI)
The backend handles the work the user never sees. When a user submits a form, for instance, the backend receives the data, validates it, decides what to do with it — perhaps calling an external API, applying business rules, or saving the result to a database — and then sends a response back to the frontend. All of this happens in milliseconds, invisibly.
More than anything, the backend is an orchestrator. It’s the central coordinator that ties everything together — receiving a request from the frontend, routing it to the right handler, talking to the database, calling external services, applying your product’s rules, and assembling a response.
To see how this orchestration works, let’s start with what happens when a single request arrives from the frontend. Every request needs three questions answered: Where should this go? (routing), Who’s asking? (authentication), and What should happen? (business logic). Each question builds on the one before it.
Request routing: where should this go?
When the frontend sends a request to the backend, something needs to decide what to do with it. That’s routing — matching each incoming request to the right piece of logic on the server.
For example, a request to /api/users/123 fetches a user’s profile, while a request to /api/orders returns their order history. Each URL pattern maps to a specific handler — the code responsible for that type of request. Routing doesn’t do anything on its own. It simply directs traffic to the right place.
Every request has two key parts: a URL (Uniform Resource Locator) that identifies the “resource” and an HTTP method — a verb that tells the backend what kind of operation to perform on that resource. The four most common methods map directly to the fundamental operations that every application needs:
GET — Read data. “Give me the user’s profile.”
POST — Create data. “Add a new order.”
PUT — Update data. “Change this user’s email address.”
DELETE — Remove data. “Cancel this reservation.”
So GET /api/users/123 means “retrieve user 123,” while DELETE /api/users/123 means “remove user 123.” Same location, different verb, completely different outcome. These four operations — creating, reading, updating, and deleting data — are so fundamental that they show up again when we discuss databases later.
Authentication: who’s asking?
Once the request has been routed, the backend needs to know who sent it. That’s authentication — verifying the identity of the user making the request.
Without authentication, every user gets the same experience. A request arrives, gets routed to a handler, and that handler does the same thing for everyone. Nothing is personalized or persistent. (This is perfectly fine when you’re building for internal alignment or running moderated user research where participants should all see the same thing.)
But for more complex applications, authentication is essential. Once you add it, the backend can store and retrieve data specific to each user — profiles, usage history, saved preferences, personalized recommendations — because it knows exactly who is making each request.
How does it work? When a user logs in, the backend verifies their credentials — checking the email and password against what’s stored in the database. If everything checks out, the backend creates a session so that every subsequent request from that browser is associated with the logged-in user. This is why you stay logged in as you navigate between pages — your browser is sending the session along with each request.
👉 This is also why clearing cookies logs a user out. Cookies are small pieces of information stored in the browser, and one of the most important is the session ID for the current user. When cookies are cleared, the browser can no longer tell the server who you are — so from the server’s perspective, you’re a stranger again.
Authentication also enables authorization: determining not just who the user is, but what they’re allowed to do. An admin might be able to delete content, while a regular user can only edit their own posts. The backend checks these permissions before executing any action.
Some tools make it easy to add authentication. For example, Lovable provides built-in authentication via Lovable Cloud, and tools that use Supabase can leverage its authentication module. Adding auth used to be a serious lift; with these tools it’s increasingly a one-prompt affair.
Business logic: what should happen?
Now the backend knows where the request is going and who sent it. The final question: what should actually happen? That’s business logic — the decision-making engine of your application, the rules that determine how your product behaves:
When a ride-share request comes in, assign it to the closest available driver.
If the user’s subscription has expired, show the upgrade screen.
Calculate the total with tax and shipping based on the user’s zip code.
Only allow admins to delete content.
Notice how many of these rules depend on the answers to the first two questions. The subscription check only works because authentication identified the user. The admin restriction only holds because authorization confirmed their role. And all four examples only execute because routing directed each request to the right handler in the first place. Together, routing, authentication, and business logic converge into product behavior — each handling a different aspect of every request the backend processes.
Here’s what matters when you’re building with AI: when you describe rules and conditions in your prompts, the AI translates them directly into business logic in the code. Every edge case you specify, every condition you define, every exception you call out makes the implementation more accurate. In AI-assisted development, you are the author of your product’s business logic, even if you never write a line of code yourself.
Connecting to databases, internal services, and external services
As part of processing each request, the backend pulls together whatever it needs to fulfill that request:
Databases store and retrieve persistent data — user profiles, order history, saved content. We’ll cover these in the next section.
Internal services. Larger products break their backend into specialized services — one for user management, another for notifications, another for search — each communicating through APIs.
External services provide functionality from third parties — Stripe for payments, OpenAI for text generation, Google Maps for location data.
In most web apps, the backend sits between the frontend and these other services, managing the communication so the frontend doesn’t have to.
When you’re moving fast, you can call some of these services directly from the frontend for simplicity — especially when using tools with limited backend support. That’s fine for testing an experience.
But if what you’re building might evolve toward production, maintain a clean separation between frontend and backend. This separation improves security, scalability, and maintainability. In that case, choose a tool with strong backend functionality — Lovable, for example, provides a rich set of backend services out of the box through Lovable Cloud.
Backend technologies in AI building tools
On the frontend, React, Tailwind, and shadcn/ui have evolved into a dominant, ubiquitous tech stack. On the backend, there’s more variety, but two technologies are worth knowing:
Next.js is a framework built on top of React that adds the ability to run code on the server — for example, preparing page content before it reaches the browser or handling backend tasks like talking to a database.
Supabase is an open-source “backend as a service” platform that bundles a database, authentication, and file storage into a single service, so you don’t have to set up and connect each piece separately.
The backend features available to you depend on which type of tool you’re using:
Prototyping tools typically have the most limited backend support. Tools like Magic Patterns are focused on making it easy to build user experiences for internal alignment and customer testing, not for building full-stack apps. Many of them have little or no built-in support for backend integration.
App builders are designed to support full-stack application development (full-stack simply means apps that have both a frontend and a backend). Tools like Lovable, v0, and Bolt typically have good support for at least one backend platform, often Supabase. Lovable has implemented a custom backend platform called Lovable Cloud that makes it easy to drop in key backend functionality such as authentication and database persistence.
Full development tools, such as Claude Code, Codex, and Cursor, can build full-stack applications using any combination of technologies. That said, it’s helpful for beginners to specify popular technologies such as Next.js and Supabase so that it’s as easy as possible to get up and running.
Databases: where data lives
Here’s a frustration you’ll encounter early: you build something, enter some data, refresh the page, and everything is gone. Your shopping cart is empty. Your form answers have vanished. Your carefully configured settings have reset.
This happens because, by default, React stores data in the browser’s memory. When you refresh the page, the app restarts and that memory is wiped clean. This works fine for things that don’t need to survive a refresh — which dropdown is open, what the user has typed into a search field, whether a modal is showing. But anything the user would expect to still be there when they come back needs something more durable.
That’s the persistence problem, and databases are the solution.
Client-side storage
Data can either be stored with the frontend (in other words client-side) or on the server in a database. Before we get into server-side databases, let’s talk about client-side data storage.
There are four ways that state can be stored on the frontend.
Hard-coded. The data is stored in a file that’s included with the source code. Because it ships with the code, it’s available every time the app loads and persists across refreshes. This is exactly what the trip-planning app we’ll explore later does — its itinerary lives in a data file, so no matter what you change in the browser, a refresh restores the original content.
Browser memory. As you interact with an app, changes are held in the browser’s memory. This is what happens when you add a comment in the trip app — the frontend loads the initial data from the source code and then tracks your change in memory. Those changes persist for as long as the user stays in that browser window and doesn’t refresh.
Local storage. Browsers have a capability called localStorage that allows data to persist across refreshes — think of it as a very simple database on the user’s computer. A simple prompt can enable your app to remember changes across refreshes using localStorage, which is useful for lightweight needs like saving preferences.
Cookies. A small amount of data can be stored in cookies. Cookies accompany every request between the frontend and backend, so it’s important to keep them lightweight to avoid adding unnecessary bandwidth. A common use for cookies, as we discussed in the authentication section above, is storing a session ID so that both the frontend and backend know who’s logged in.
Understanding these approaches helps you debug why data might be disappearing (or not) in your apps, and is useful for lightweight needs like saving preferences or keeping something functional without a backend.
But what if you need data accessible from any device, or need to handle large amounts of data? That’s where server-side databases come in.
Server-side storage
A server-side database stores data on remote servers rather than in the user’s browser. You can think of it as a collection of tables, similar to spreadsheets, where each row is a record (a user, an order, a post) and each column is a field (name, email, date created). The database lives on a server, which means it’s accessible from any device and can handle large volumes of data — making it the right choice for anything that needs to be shared across users or sessions, such as user profiles, transactions, and content.
When we talked about the backend connecting to databases in the previous section, this is what we meant: the backend reads from and writes to a server-side database on behalf of the user.
Many production apps use both client-side and server-side storage: store preferences locally for speed, store important data on the server for reliability. For early-stage builds, client-side storage or even hardcoded data files are often sufficient — you’re testing the experience, not building production infrastructure. But as what you’re building matures, especially when you start testing with real users who expect their data to persist, you’ll want a real database behind your app.
CRUD: the four database operations
Regardless of what kind of database you use, nearly every interaction falls into one of four operations:
Create — Add new data. A user signs up and their profile is created.
Read — Retrieve existing data. A user opens the app and sees their dashboard.
Update — Modify existing data. A user changes their email address.
Delete — Remove data. A user cancels their subscription.
These are collectively known as CRUD, and if they look familiar, they should — they map directly to the HTTP methods we covered in routing: POST creates, GET reads, PUT updates, and DELETE removes. This isn’t a coincidence. The entire stack is organized around these four operations, from the request that arrives at the backend to the query that hits the database. When you understand CRUD, you understand the basic vocabulary of how data moves through your application.
APIs: how everything talks to each other
An API (Application Programming Interface) is a structured way for one piece of software to request something from another. You’ve already seen this pattern: the frontend sends a request to the backend, and the backend sends a response. An API simply formalizes that exchange — defining what you can ask for, how to ask for it, and what you’ll get back.
APIs aren’t limited to communication within your own app. They’re also how your app talks to external services. When your app needs to generate text via an LLM, process a payment, or look up a location, it does so by calling an API — sending a request to someone else’s server and getting a result back.
REST APIs and endpoints
Most APIs you’ll encounter follow a convention called REST (Representational State Transfer). REST APIs use the same HTTP methods and URL patterns you already learned in routing — and the same operations you saw in CRUD:
GET /api/trips— Retrieve all tripsPOST /api/trips— Create a new tripGET /api/trips/123— Retrieve the trip with ID 123PUT /api/trips/123— Update that tripDELETE /api/trips/123— Remove that trip
The combination of an HTTP method and an endpoint (the URL) defines exactly what you’re asking for. This pattern likely looks familiar — it rhymes with the create, read, update, and delete (CRUD) pattern that we saw in the databases section. Whether you’re routing a request, querying a database, or calling an external API, the underlying structure is similar.
API keys
When you use someone else’s API — OpenAI for AI features, Google Maps for location data, Stripe for payments — they give you an API key: a unique code that identifies your application. You can think of an API key as a password that lets you log in to use the API. The key lets the provider track your usage, enforce rate limits, and charge you for what you use.
Similar to passwords, API keys should be kept secret. They should never appear in your frontend code, where anyone could inspect them in the browser. They also should not be stored in your codebase, regardless of whether that codebase is maintained in your AI building tool or GitHub.
Instead, API keys are stored in environment variables — configuration values kept securely outside your codebase. Your backend reads the key from the environment variable when it needs to call the external API, so the key never reaches the browser.
👉 Most API calls should be made from the backend, not the frontend. This keeps API keys secure, gives you control over error handling, and prevents users from inspecting or tampering with requests in the browser. However, some APIs are designed to be called directly from the frontend — analytics tracking, for example, needs to fire from the browser to accurately capture user behavior. And while you’re moving fast, you may decide to call an API client-side just to get things working quickly before wiring up a proper backend.
APIs are one of the most practical ways to make what you build feel real. Instead of faking complex functionality, you can often plug into an existing service and focus your effort on the user experience.
Inside a real app
Now that we’ve covered how software is structured in theory, let’s see how it works in practice. Say we’d like to build a collaborative trip-planning feature for a travel site such as Tripadvisor or Airbnb. We can write a brief but powerful prompt that gives the AI clear context:
# Build a Trip Itinerary Feature
## Functionality
Build a trip itinerary page with the following sections:
* Trip header - a large cover photo with the destination name, trip name, date range, and a row of traveler avatars
* Logged-in user indicator - display the current user's identity in the upper right
* Traveler modal - clicking on a traveler avatar in the header opens a modal showing that traveler's full profile info
* Day tabs - a tab bar with one tab per day of the trip. Clicking a day tab switches the view to that day's itinerary items
* Itinerary cards - each item for the selected day displayed as a card with its relevant details
* Comments system - each card shows a collapsed comment indicator (count + chat icon) by default. Clicking the indicator expands/collapses a comments section that:
- Displays existing comments for that item
- Includes an input area to add a new comment
- On submit, appends the comment to that item's comments array with the current timestamp and displays it immediately
## Design
* Design the page based on the selected design system.
* Make sure to preserve the aspect ratio for all images including avatars.
## Data
* Populate the page with real trip content using the attached JSON file.
* Make sure to use all of the data provided.
* The current logged-in user is id='traveler-1'.
* Save the data to a separate file called itinerary.json.The prompt assumes that your preferred AI coding tool has a way to select a design system. If not, you can remove that line altogether or provide some screenshots as reference.
The prompt also links to a JSON data file with sample data for the itinerary.
Download the data → paris_trip.json
💡 The data downloads as a JSON file. Some AI building tools don’t allow file uploads — if yours doesn’t, rename it to paris_trip.txt and upload that instead. If that still doesn’t work, open the file, copy everything in it, and paste it directly into your prompt.
I built this version with v0.dev and made the full source public so you can follow along:
See it running → Live app on v0
Read the code → GitHub repository
Here’s the generated app we’ll be walking through:
Let’s walk through the code together. The goal isn’t to understand every line of generated code, but to build pattern recognition. After this walkthrough, when you open your own projects, the structure should look familiar rather than foreign.
Also note that this app is frontend only — there’s no code that connects to a backend, database, or APIs. Everything runs in the browser. That’s a common and completely reasonable starting point.
The file structure
When you open the code for this app, here’s what you see:
trip-itinerary-feature/
├── app/ ← The Next.js "App Router"
│ ├── layout.tsx ← The outer shell: fonts, metadata, <html>/<body>
│ ├── page.tsx ← The entry point: loads data, renders the page
│ └── globals.css ← Global styles, theme colors, Tailwind setup
├── components/ ← One file per major UI element
│ ├── itinerary-page.tsx ← The conductor: assembles the whole page
│ ├── trip-header.tsx ← The cover banner (photo, title, travelers)
│ ├── itinerary-card.tsx ← Each activity card + its comment thread
│ ├── traveler-modal.tsx ← Traveler profile pop-up
│ └── ui/ ← Pre-built shadcn/ui components
│ ├── button.tsx
│ ├── tabs.tsx
│ ├── dialog.tsx
│ ├── badge.tsx
│ └── avatar.tsx
├── data/
│ └── itinerary.json ← All the trip data
├── lib/
│ ├── types.ts ← Data structure definitions
│ └── utils.ts ← Tailwind class helper
├── public/ ← Static assets (icons, images)
├── package.json ← The app's dependencies
└── tsconfig.json ← TypeScript configurationThis structure follows a pattern you’ll see in most generated codebases. The components/ folder holds one file per UI element — and the file names match what you see on screen (trip-header, itinerary-card, traveler-modal). The data/ folder holds the content. The lib/ folder holds helper functions and the definitions that describe the shape of the data. The components/ui/ folder contains pre-built shadcn/ui components — generic building blocks like buttons and tabs that the tool includes by default.
This app is built with Next.js — the React-based framework we mentioned in the backend section. Even though this particular app is frontend only, tools like v0 generate Next.js apps by default, and Next.js uses a special app/ folder (called the “App Router”) to define the entry point and overall page shell. We’ll look inside it in a moment.
This gives you a practical mental model: if you need to change how something looks, go to components/. If you need to change what content appears, go to data/. More broadly, the folder names are your map: app/ is where the app starts, components/ is the interface, data/ is the content, and lib/ holds the behind-the-scenes helpers. When something needs to change, the folder name usually tells you where to look first.
Notice the file extensions. .tsx means it’s a TypeScript file that contains both logic and visual markup. Each .tsx file contains one or more React components that are used like LEGO blocks to construct your UI. The .ts files are pure logic — no visual elements. .css is styling. .json is pure data. File extensions are your first clue about what a file does.
You’ll also notice this app is a bit more consolidated than the tidy tree above might suggest. The day tabs and the day heading live inside itinerary-page.tsx rather than in their own files, and the comment thread is built into itinerary-card.tsx. That’s completely normal — the AI often bundles closely related pieces together. If you’re ever having trouble understanding the code because too much is crammed into one file, ask the AI to reorganize it into a more readable structure. Use a prompt like this:
Refactor the code so each major UI element is its own component file in
components/. Keep data in data/, types in lib/, and helper functions in lib/.
Make the code easy to understand for a novice programmer.How the app starts
In a Next.js app, there are two files that get things going, and both live in the app/ folder.
The first is app/layout.tsx. Think of it as the outer shell of every page — it sets up the fonts, the page metadata (like the title that shows in the browser tab), and wraps everything in the standard <html> and <body> tags. It’s mostly boilerplate, and you’ll rarely need to touch it.
The second is app/page.tsx, and this is where the page actually begins. It’s tiny — it imports the trip data and hands it to the component that does the real work:
import itineraryData from "@/data/itinerary.json"
import { ItineraryPage } from "@/components/itinerary-page"
export default function Page() {
const itinerary = itineraryData.itinerary
return <ItineraryPage itinerary={itinerary} />
}That one file tells you two important things: the data comes from data/itinerary.json, and the main user interface lives in a component called ItineraryPage, over in components/itinerary-page.tsx.
ItineraryPage is the conductor. It assembles the UI components into a full page. Here’s a simplified view:
<div>
{/* Logged-in user indicator — your avatar in the top right */}
<TripHeader /> ← The large Eiffel Tower banner
<Tabs> ← Day 1 / Day 2 / Day 3
{/* Day heading — "Arrival & Settling In" with the date */}
<ItineraryCard /> ← Each activity for the selected day
</Tabs>
<TravelerModal /> ← Pop-up when you click a traveler avatar
</div>If you’re wondering “what appears on screen and in what order?”, itinerary-page.tsx is the place to look. The code reads top to bottom, just like the page. And most of the component names map directly to a file — <TripHeader /> is defined in components/trip-header.tsx, <TravelerModal /> is in components/traveler-modal.tsx, and so on. A couple of things (the logged-in indicator in the top-right, the day heading) are written directly inline here rather than pulled out into their own components — another example of the AI keeping small, related pieces together.
ItineraryPage also manages some of the page’s state — for example, which traveler’s modal is currently open. But notice that not all state lives in one place. Which day is selected is handled by the Tabs component, and each activity card keeps track of its own comments. This is a good thing to internalize: state lives at whatever level needs it, not always at the top.
👉 You’ll see
"use client"at the top of some files. That’s just Next.js marking which parts run in the browser to handle clicks, typing, and toggles — the AI adds it wherever it’s needed, so it’s not something you have to manage..
The data layer
One of the most important things to understand about this app is that the data and the display are separate.
Our app is using the “hard-coded” approach we discussed in the Client-side storage section above. The file data/itinerary.json contains all the trip information — the destination name, dates, travelers, and every activity for every day — saved in a JSON file that ships with the app’s codebase. This is the data we provided in the prompt above — the JSON file we attached (or pasted directly into the prompt) is exactly what the AI saved to data/itinerary.json.
Here’s a small piece of the JSON file:
{
"itinerary": {
"name": "Magical Spring in Paris",
"destination": "Paris, France",
"travelers": [
{
"id": "traveler-1",
"firstName": "David",
"lastName": "Chen",
"travelStyle": "Art & Culture Enthusiast"
}
],
"days": [
{
"date": "2025-05-15",
"title": "Arrival & Settling In",
"items": [
{
"name": "Hôtel des Grands Boulevards",
"type": "accommodation",
"startTime": "15:00",
"duration": "overnight",
"rating": 4.5,
"reviewCount": 1247,
"tags": ["Luxury", "Historic", "Central Location"],
"description": "Elegant boutique hotel in the heart of Paris..."
}
]
}
]
}
}Providing well-structured sample data is one of the most effective things you can do when prompting. It shows the AI exactly what you want to build, and it plays an important role in helping the AI generate good code.
In fact, this structure directly shaped the code. In lib/types.ts, there’s a set of interface declarations that describe the data. For example, this is the interface for each item in the itinerary:
export interface ItineraryItem {
id: string
name: string
type: string
startTime: string
duration: string
rating: number
reviewCount: number
tags: string[]
photo: { url: string }
description: string
notes?: Note[]
}How did the AI know that each itinerary item should include name, type, startTime, rating, tags, etc.? Because the AI was able to discern the structure of the data — the data schema — from the sample JSON data we provided.
This approach also helped the AI decouple the data structure from the user interface. That separation matters because it lets you update the data without touching any UI code.
For example, if you want to change the hotel name, add a fifth traveler, or tweak a rating, you can simply edit data/itinerary.json (or ask the AI to do it). When you tell the AI “change the hotel to a 4.8-star rating” or “add a traveler named Priya,” it knows to update data/itinerary.json, and the UI automatically reflects whatever data it receives.
This separation between data and display is critical as your applications get more complex.
Walking through the UI
Let’s trace how specific things you see on screen connect to specific files in the codebase.
The cover banner → trip-header.tsx
The large Eiffel Tower image at the top of the app — with the trip title, destination, date range, and traveler avatars overlaid — is the TripHeader component. It receives the data it needs as props from ItineraryPage: the full itinerary (cover photo, name, destination, date range, and travelers) plus a function to call when someone clicks an avatar. Those clickable avatars are what open the traveler modal we’ll get to shortly.
The day tabs → the Tabs component
The horizontal navigation that lets you switch between Day 1, Day 2, and Day 3 is built with the shadcn/ui Tabs component (from components/ui/tabs.tsx), rendered inside itinerary-page.tsx. This is a clean example of state in action.
The app needs to track which day is currently selected. Rather than wiring that up by hand, the Tabs component manages it for us: when you click a different tab, it updates which day is active, and React automatically re-renders the page to show that day’s activities. You didn’t tell the page to clear Day 1’s content and load Day 2’s. You clicked a tab, one value changed, and the UI updated to match. This is actually why React is called React — it makes the user interface react to changes in state.
This also demonstrates reuse. Rather than hard-coding three separate tabs, the code uses days.map() to loop through the days array and generate one tab per day:
{itinerary.days.map((day, index) => (
<TabsTrigger key={day.date} value={day.date} className="...">
<span>Day {index + 1}</span>
<span>{format(parseISO(day.date), "EEE, MMM d")}</span>
</TabsTrigger>
))}The key piece here is .map() — that’s the looping. Don’t let the notation throw you; it’s not very human-readable at first glance, with its arrow (=>), parentheses, and nested tags. But the idea underneath is simple. In JavaScript, .map() means “for each item in this list, produce something.” So itinerary.days.map(...) reads as “for each day in the trip, create one tab” — and each TabsTrigger in the code is exactly that: one clickable tab in the bar. Whenever you spot .map() in your app’s code, you’re looking at a loop over a list.
Each tab displays the day number and a formatted date. If the trip had six days instead of three, this code would automatically generate six tabs. No code changes needed. This is why separating data from display pays off.
The day heading → inside itinerary-page.tsx
Below the day tabs, a header shows the selected day’s date, title, and how many stops it has. It’s written directly inside itinerary-page.tsx, and it’s a good, simple place to see how Tailwind styling reads in practice. Here’s the block:
<div className="mb-6">
<p className="text-xs text-muted-foreground uppercase tracking-wider font-medium mb-1">
{format(parseISO(day.date), "EEEE, MMMM d, yyyy")}
</p>
<h2 className="text-2xl font-semibold text-foreground">{day.title}</h2>
<p className="text-sm text-muted-foreground mt-1">
{day.items.length} {day.items.length === 1 ? "stop" : "stops"}
</p>
</div>This displays a small, muted date label, a large bold title, and a subtitle with the number of stops. You can see how Tailwind makes this happen — every class does one thing. text-2xl sets the font size. font-semibold makes it bold. text-foreground sets the color. mb-6 adds spacing below. text-sm makes the subtitle smaller. text-muted-foreground gives it a softer color. mt-1 adds a small gap above it. You can read the intent of the styling even without knowing CSS — and when you ask the AI to “make the day title larger,” it would change text-2xl to text-3xl or text-4xl.
There’s even a tiny bit of business logic hiding in plain sight: day.items.length === 1 ? "stop" : "stops" shows “1 stop” but “3 stops,” so the label is always grammatically correct. You’ll see this same pattern — short, readable class names describing exactly what they do, with small logic woven in — throughout the app.
The activity cards → itinerary-card.tsx
Each activity — the hotel, the cafe, the museum, the river cruise — is rendered by the same ItineraryCard component with different data. One piece of code, rendered many times, each time with different props. This is the essence of reusable components.
Inside ItineraryCard, several concepts converge:
Business logic determines visual style. A lookup table called TYPE_CONFIG maps each activity type to a label, an icon, and a color. Accommodations become “Hotel” with a bed icon. Restaurants become “Restaurant” with a utensils icon. Museums get a landmark icon; landmarks get a camera icon. These are decision rules — business logic — that drive what the user sees.
const TYPE_CONFIG = {
accommodation: { label: "Hotel", icon: Bed, color: "bg-blue-50 text-blue-700 ..." },
restaurant: { label: "Restaurant", icon: Utensils, color: "bg-amber-50 text-amber-700 ..." },
museum: { label: "Museum", icon: Landmark, color: "bg-emerald-50 text-emerald-700 ..." },
landmark: { label: "Landmark", icon: Camera, color: "bg-rose-50 text-rose-700 ..." },
// ...
}Libraries and helpers transform raw data. The dates and numbers you see on screen aren’t the raw values from the data. Dates run through a well-known library called date-fns — parseISO reads the stored date and format turns "2025-05-15" into "Fri, May 15". The review count uses JavaScript’s built-in .toLocaleString() to turn 1247 into 1,247, and the rating uses .toFixed(1) so 4.5 always shows one decimal place. Notice the pattern: rather than reinventing date formatting, the AI reached for a battle-tested library — exactly what an experienced engineer would do.
Tags render dynamically. Each activity has an array of tags — ["Luxury", "Historic", "Central Location"] — and the code loops through them, rendering a small pill-shaped label (a shadcn/ui Badge) for each one. Add a tag to the data, and it appears on screen. Remove one, and it disappears. No layout changes needed.
The comment system → built into itinerary-card.tsx
Each activity card has a comment thread that travelers can expand, read, and add to. In the data these are called notes, and the thread is built right into the ItineraryCard component — showing each comment’s author avatar, name, timestamp, and message.
The comments are interactive: when you type a message and hit Enter, it appears instantly on screen — no page refresh needed. Each card keeps its own list of comments in state. But notice that these comments live in the browser’s memory, not in a database. If you refresh the page, anything you added disappears and the original comments return. This is typical for a first version — it demonstrates the experience of commenting without the complexity of storing data on a server. Making comments like these persist is exactly the kind of thing you’d add a database for later.
The traveler modal → traveler-modal.tsx
Click on any traveler avatar in the header, and a modal pops up showing their name, photo, and travel style (built on the shadcn/ui Dialog component). This is another piece of state in action. ItineraryPage keeps track of which traveler was clicked (selectedTraveler) and whether the modal is open (modalOpen). Click an avatar, and it stores that traveler’s data and opens the modal. Close it, and the state resets.
This pattern — a piece of state that controls whether something is visible — is one of the most common patterns in React. Modals, dropdown menus, tooltips, sidebars, notification banners — they all work this way. One state variable, toggled on and off.
Global styles and theming → app/globals.css
The app/globals.css file sets up Tailwind and defines a theme — a set of color variables like --primary, --background, --foreground, and --muted-foreground. These variables are what allow the UI to reference colors like bg-primary or text-muted-foreground without specifying exact values. (This app defines its colors using a modern format called OKLCH, and it even ships light and dark versions of every color, so it supports dark mode automatically.)
The practical implication: if you want to change your app’s overall color scheme — say, from blue to green — changing these variables in app/globals.css updates every element that references them. Define a color once, use it everywhere. This is why theming works: a single change cascades through the entire interface.
Putting it all together
Let’s trace what happens when someone visits this app:
The browser loads the app. Next.js renders
app/layout.tsx(the outer shell) andapp/page.tsx(the page itself).page.tsximports the itinerary data fromdata/itinerary.jsonand passes it to theItineraryPagecomponent.ItineraryPagerenders its children: the logged-in indicator,TripHeader, the dayTabs, a day heading, and anItineraryCardfor each activity in the selected day.Each
ItineraryCardreceives its specific data (hotel, cafe, museum, cruise) as props.When the user clicks Day 2, the
Tabscomponent switches to Day 2’s content and React re-renders.When the user clicks a comment toggle, that card’s comment thread expands.
When the user clicks a traveler avatar,
selectedTravelerupdates inItineraryPageand theTravelerModalappears.
Notice what’s not happening. There’s no backend server processing requests. No database storing data. No API calls fetching information. The data is stored in data/itinerary.json and everything runs in the browser.
This is typical for a first version: it demonstrates the user experience without the complexity of real data infrastructure. The frontend is fully built. The backend and database layers aren’t needed yet. When you’re ready to make something like this real — so comments persist, travelers log in, and data lives on a server — that’s the natural next step, and the tools we discussed earlier make it increasingly approachable.
What this means for what you build
Now that you’ve walked through a real codebase, you have a mental model for how the pieces fit together. You don’t need to memorize any of it — the goal is pattern recognition. When you open your own project’s code and see folders and files, you’ll have a sense of what you’re looking at and where to point the AI when something needs to change.
And of something doesn’t make sense? Just ask the AI to explain what its doing and you’ll get a surprisingly detailed and helpful response.
That ability — knowing where to look — is often the difference between getting unstuck in thirty seconds and spending an hour reprompting in circles. At the start, we said you don’t need to be a mechanic to drive a car, but it helps to know what you’re looking at when you pop the hood. Now you can do that with your own software.
Want to go deeper? This piece is adapted from my AI Prototyping course on Reforge, which walks product managers through building with AI end to end — from your first prompt to a working, testable product.














Reading the code may be the wrong bar. The durable skill is reading the architecture: what the system should do, where it breaks, what counts as done. That spec-level judgment is what separates builders who ship from builders who pile up code nobody can review. Are you seeing non-technical builders actually develop it, or just prompt faster?