Gemini Structured Output With Zod in Next.js
Describe the JSON once in Zod, send z.toJSONSchema() to Gemini as responseJsonSchema, then validate the reply with safeParse and retry once with the errors.
The short answer to CSS Flexbox vs Grid: use Flexbox when you're lining things up in one direction, a row or a column, and want the content to decide the sizes. Use Grid when you need rows and columns at the same time, or when the layout should decide where things go. Most real pages use both: Grid for the overall layout and card lists, Flexbox for the small pieces inside them. Below I show how I decide, with code you can copy, plus the mistakes I made while learning both.
I used to pick one at random and fight it until the layout worked. Once I understood the "one dimension vs two dimensions" idea, choosing became easy. Every snippet here passes a CSS validator, and I rendered the layouts in headless Chrome to check they look the way I describe.
Flexbox works along one main axis. Items flow in a line, and you control how they share the space, how they line up and whether they wrap. Wrapped lines don't line up with each other, though, because each line is laid out on its own.
Grid works on two axes at once. You define columns and rows on the container, and items are placed into that structure. Every item in a column lines up with the items above and below it.
Another way I think about it:
A header with a logo on the left and links on the right is a single row, which is the perfect Flexbox job:
<header class="site-header">
<a class="logo" href="/">mfaysal.dev</a>
<nav aria-label="Main">
<ul class="nav-links">
<li><a href="/blog">Blog</a></li>
<li><a href="/projects">Projects</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
.site-header {
display: flex;
align-items: center; /* vertical centring on the cross axis */
justify-content: space-between; /* logo left, links right */
gap: 1rem;
padding: 1rem 1.5rem;
}
.nav-links {
display: flex;
flex-wrap: wrap;
gap: 1.25rem;
list-style: none;
margin: 0;
padding: 0;
}
justify-content controls spacing along the main axis (horizontal here), and align-items controls the cross axis (vertical here). gap adds space between items without margin hacks. It's been supported for Flexbox in all major browsers for years now.
A search input that stretches next to a fixed-size button is another classic Flexbox pattern:
<form class="search-bar" action="/search">
<input name="q" type="search" placeholder="Search posts" aria-label="Search posts">
<button type="submit">Search</button>
</form>
.search-bar {
display: flex;
gap: 0.5rem;
}
.search-bar input {
flex: 1; /* take all the leftover space */
min-width: 0; /* allow it to shrink below its default width */
}
flex: 1 tells the input to grow into whatever space is left. The min-width: 0 line fixes a very common bug: flex items won't shrink smaller than their content by default, so without it an input can overflow on small screens.
This is the Grid snippet I use most. It fits as many columns as there's room for, each at least 220px wide:
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1.5rem;
}
<div class="card-grid">
<article class="card">
<h2>Quizwright</h2>
<p>AI quiz generator.</p>
<a href="/projects">View</a>
</article>
<article class="card">
<h2>StudyPilot</h2>
<p>Offline study planner with a longer description that wraps onto more lines.</p>
<a href="/projects">View</a>
</article>
<article class="card">
<h2>Kinetica</h2>
<p>Canvas design and animation editor.</p>
<a href="/projects">View</a>
</article>
</div>
On a wide screen you get several columns; on a phone, one. No media queries needed. There's a small difference between the two keywords: auto-fill keeps empty column tracks when there are only a few cards, so they stay card-sized. auto-fit collapses the empty tracks, so the existing cards stretch to fill the row. I usually prefer auto-fill, because three cards stretched across a huge monitor look odd.
For a header, sidebar, main content and footer, named areas make the CSS read almost like a picture of the layout:
<div class="page">
<header>Header</header>
<aside>Sidebar</aside>
<main>Main content</main>
<footer>Footer</footer>
</div>
.page {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
min-height: 100vh;
gap: 1rem;
}
.page > header { grid-area: header; }
.page > aside { grid-area: sidebar; }
.page > main { grid-area: main; }
.page > footer { grid-area: footer; }
@media (max-width: 700px) {
.page {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto auto;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
}
}
On small screens, the media query switches to one column and moves the sidebar below the main content just by changing the area map. The HTML order stays the same, which matters for keyboard and screen reader users.
When some boxes should be wider or taller than others, Grid's span keyword is exactly right:
.dashboard {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 140px;
gap: 1rem;
}
.widget--wide { grid-column: span 2; }
.widget--tall { grid-row: span 2; }
.widget--hero { grid-column: span 4; }
When I rendered this, a "wide" widget took two of the four columns, a "tall" one took two rows, and the small widgets filled the gaps in order. My offline study planner, StudyPilot, has a dashboard where each widget can be 1 to 4 columns wide and 1 to 3 rows tall. That kind of layout is a two-dimensional problem, which is Grid territory. Flexbox can't make an item span two rows.
The best layouts often nest them. In the card grid above, Grid places the cards, and each card uses Flexbox inside to keep its "View" link at the bottom, even when descriptions have different lengths:
.card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1.25rem;
border: 1px solid #2c3550;
border-radius: 12px;
}
.card a {
margin-top: auto; /* pushes the link to the bottom of every card */
}
margin-top: auto on a flex item eats up all the free space above it, so the link sits at the bottom of every card. Because Grid makes all cards in a row the same height by default, the links line up across the row too.
Centring something in a box is no longer hard. Both of these work:
/* Flexbox */
.center-flex {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
}
/* Grid: one line does both axes */
.center-grid {
display: grid;
place-items: center;
min-height: 300px;
}
I use the Grid version when I just want something centred, because place-items: center is one line. I use the Flexbox version when there are several items in a row that should be centred together.
With flex-wrap: wrap, the last row often ends up with stretched or oddly sized items, because every flex line is sized separately. If you want neat columns, that's a sign you need Grid.
Flex and grid items have an automatic minimum size based on their content. Long words, URLs or inputs can then overflow the container. min-width: 0 on the item (or minmax(0, 1fr) for grid columns) lets them shrink.
display: flex and display: grid go on the parent, and only affect its direct children. Grandchildren aren't laid out by it, unless you also make the child a flex or grid container (or use subgrid).
In Flexbox, justify-content follows the main axis, which changes when you set flex-direction: column. If your centring suddenly works "the wrong way", check the direction.
order and grid placement change where things appear, not the order a keyboard or screen reader follows. Keep the HTML in a logical order.
Neither is better. They solve different problems. Grid is for two-dimensional layouts, Flexbox is for one-dimensional ones, and they work well together.
You could build almost any layout with Grid, but simple rows like a nav bar or a button group take less code with Flexbox, and Flexbox handles items of unknown width more naturally.
grid-template-columns: subgrid (or rows) lets a nested grid reuse its parent's tracks, so content inside different cards can line up with each other. It's supported in all current major browsers.
Less often. Patterns like repeat(auto-fill, minmax(220px, 1fr)) and flex-wrap adapt on their own, but bigger changes, like moving a sidebar below the content, still use media queries or container queries.
Flexbox vs Grid isn't a competition. Reach for Flexbox for things in a line, Grid for things in rows and columns, and combine them freely. Grid for the page and the card list, Flexbox inside each card, is a pattern you'll use constantly. For badges, overlays and sticky headers on top of that layout, see CSS position: absolute vs relative.
If you want to go deeper, MDN has clear guides on basic concepts of grid layout and how it relates to Flexbox.
// note
This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.
If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.
Describe the JSON once in Zod, send z.toJSONSchema() to Gemini as responseJsonSchema, then validate the reply with safeParse and retry once with the errors.
Step by step: a Gemini key from Google AI Studio in .env.local, a server-only helper, a Next.js route handler, Vercel environment variables and a leak test.
A small offline habit tracker in plain HTML and JavaScript: habits saved as JSON in localStorage, streaks from local dates, a 7-day row and a JSON backup.