I Built a Merch Store with Next.js + Printful — Here's How It Works

Compiled Designs·

Every developer thinks about building a side project that actually makes money. Most of us have a graveyard of half-finished apps and abandoned repos. When I decided to build Compiled Designs — a merch store for software engineers — I wanted to do it differently. I wanted to ship something real, with real payments, real fulfillment, and real customers.

This post is a technical walkthrough of how the store works under the hood. If you've ever thought about building an e-commerce side project, or you're curious about integrating print-on-demand into a Next.js app, this is for you.

The Stack

Compiled Designs runs on Next.js 16 with the App Router, TypeScript, React 19, and Tailwind CSS. Payments go through Stripe Checkout. Products are sourced and fulfilled by Printful, a print-on-demand service that handles printing, packing, and shipping. The whole thing is deployed on Vercel.

I chose this stack for a few reasons. Next.js gives me server-side rendering out of the box, which matters for SEO — especially for a store that needs to rank for terms like "developer t-shirts" and "programmer gifts." The App Router's server components mean product pages load fast because most of the rendering happens on the server. And Vercel makes deployment trivial with automatic previews on every push.

How Print-on-Demand Works

If you haven't worked with print-on-demand before, the concept is simple: you design a product, upload the design to a service like Printful, and they handle everything else. When someone buys a shirt from Compiled Designs, Printful prints it, packs it, and ships it directly to the customer. I never touch inventory.

The trade-off is margin. Print-on-demand costs more per unit than bulk ordering. A t-shirt that might cost $5 to produce in bulk costs $15 to $20 through Printful. But the upside is enormous: zero inventory risk, no upfront capital, and no garage full of unsold shirts. For a side project, that trade-off is worth it.

The Product Data Pipeline

Products in Compiled Designs are sourced from the Printful API. Each product has sync variants — combinations of size and color that map to specific Printful SKUs. The challenge is that Printful's API returns products in a format that doesn't map cleanly to how you'd want to display them on a storefront.

For example, Printful treats "Built with Intent T-Shirt - Light" and "Built with Intent T-Shirt - Dark" as separate products. But on the storefront, those should be a single product with a color toggle. The product data layer consolidates these variants into a unified product model with a variant map that connects size and color selections to the correct Printful sync variant ID.

Product data is cached using Next.js unstable_cache with a configurable TTL. A Vercel cron job refreshes the cache every 15 minutes during a window between 3 and 5 AM, so the storefront always has fresh data without hammering the Printful API on every page load.

Stripe Checkout Integration

Rather than building a custom checkout form, I use Stripe Checkout — Stripe's hosted payment page. When a customer clicks "Checkout" in the cart, the frontend sends a POST request to an API route that creates a Stripe Checkout session. The session includes line items, shipping options, and metadata about the order.

The key architectural decision here is that order creation happens in the Stripe webhook handler, not on the success page. This is important. If you create orders on the success page, you'll lose orders when customers close their browser before the page loads, or when the redirect fails. The webhook is Stripe's guarantee that payment was completed.

When Stripe fires the checkout.session.completed webhook, the handler verifies the webhook signature, extracts the order details from the session metadata, creates the order in Printful via their API, persists the order locally, and sends a confirmation email to the admin via Resend. This all happens server-side with no client involvement.

The Cart: Client-Side State with localStorage

The cart is implemented as a React context provider that persists to localStorage. I considered server-side cart storage, but for a small merch store it's unnecessary complexity. LocalStorage is simple, requires no database, and works well for the use case.

The CartProvider wraps the entire app and exposes functions for adding items, removing items, updating quantities, and clearing the cart. Cart state is serialized to localStorage on every change and rehydrated on page load. The one gotcha with this approach is hydration mismatch — the server doesn't know about localStorage, so you need to handle the initial render carefully to avoid React hydration warnings.

Deployment and Infrastructure

The site is deployed on Vercel with automatic deployments from the main branch. Environment variables for Stripe, Printful, Resend, and other services are configured in the Vercel dashboard. The Stripe webhook endpoint is configured to point to the production URL.

One thing I'd do differently if starting over: I'd set up preview environment webhooks earlier. Testing the full checkout flow in preview deployments requires a separate Stripe webhook endpoint, which adds some configuration overhead.

What I'd Change

If I were starting this project from scratch, a few things I'd do differently. First, I'd use a database from day one instead of file-based order storage. Even something simple like Turso or PlanetScale would make order management much easier as the store scales. Second, I'd implement proper image optimization earlier — product images from Printful can be large, and Next.js Image component with proper sizing makes a measurable difference in page speed. Third, I'd build the email capture flow before launch, not after. An email list is the most direct channel to potential customers, and every day without it is lost signups.

Lessons Learned

Building an e-commerce side project is different from building a SaaS or a developer tool. The feedback loop is slower — you can't just ship a feature and watch metrics move. You need traffic, and traffic takes time to build through SEO, content, and community presence.

The biggest lesson: the code is the easy part. The hard part is everything around it — product photography, SEO optimization, writing product descriptions that actually sell, building a brand presence, and doing the unglamorous work of sharing your store in communities without being spammy about it.

If you're thinking about building something similar, my advice is to keep the technical scope small and spend more time on the marketing fundamentals. A simple store that people can find will always outsell a technically impressive store that nobody knows exists.

Check out the store at compiled-designs.com/shop to see the finished product, and feel free to reach out if you have questions about the architecture.

next.jsprintfulstripeprint on demanddeveloper side projecte-commerce