Stop 14 · Payments & Third-Party IntegrationsTopics · 1/6

Stripe Integration

Part of the Full Stack Developer Roadmap

In short

Taking money online sounds scary, but with Stripe your server never touches a card number. Your job is to create the checkout, then trust only Stripe's webhook when deciding an order is paid.

Adding real payment processing to a full-stack app — a frontend checkout UI backed by a backend that creates charges/subscriptions and never touches raw card details directly.

The blob character taps a card on a handheld card machine held out by a waiter, beside an empty thali.

Think of it like this

The card machine at your table

At a good restaurant, the waiter brings the card machine to your table. The owner never sees your PIN, and the bill is marked paid only when the machine says 'approved', not when the customer says 'done, I paid'. Stripe is the machine, and its webhook is that approval slip.

Watch it happen

Checkout to 'paid', the safe way

Browser
Your API
Stripe
Postgres

POST /api/checkout

1/7The user clicks Pay. The browser sends only the cart's item ids, never prices.

The big ideas

What you need to know

Cards never touch your server

Stripe Checkout or Elements collect card details on Stripe's side, which keeps most card-security work off your plate.

Prices come from the server

Never trust an amount sent by the browser. Look up prices or Stripe Price ids on the server.

Webhooks decide 'paid'

The success redirect can be skipped or faked. The signed webhook event is the reliable signal.

Test mode first

Use test keys and the test card 4242 4242 4242 4242 with any future date. The Stripe CLI forwards webhooks to localhost.

See it in code

Walk through the code

ts
1import Stripe from "stripe";2const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);3 4app.post("/api/checkout", requireUser, async (req, res) => {5  const order = await createPendingOrder(req.user.id, req.body.itemIds); // prices from the DB6  const session = await stripe.checkout.sessions.create({7    mode: "payment",8    line_items: order.items.map((i) => ({ price: i.stripePriceId, quantity: i.qty })),9    success_url: `${process.env.APP_URL}/orders/${order.id}`,10    cancel_url: `${process.env.APP_URL}/cart`,11    metadata: { orderId: String(order.id) },12  });13  res.json({ url: session.url });14});15 16// Webhook: stripe.webhooks.constructEvent(rawBody, req.headers["stripe-signature"], WEBHOOK_SECRET)

1/4Your own helper saves a pending order using prices from your database, not from the request.

The webhook route needs express.raw({ type: 'application/json' }) so the signature check sees the exact bytes Stripe sent.

Save yourself the pain

Common mistakes

Oops

Marking the order paid when the success page loads.

Do this instead

The success page only shows status. Mark it paid in the webhook, after verifying Stripe's signature.

Oops

Parsing the webhook body with express.json() before verifying it.

Do this instead

Use express.raw on that one route. The signature check fails once the body has been parsed and re-serialised.

Oops

Putting the secret key in frontend code.

Do this instead

Only the publishable key may reach the browser. The secret key lives in backend env vars.

Your mission

Take a test payment

~45 min

Quick check

A user closes the tab right after paying, before the success page loads. How does your app learn the order is paid?

Go deeper

  1. 1Build a checkout flow using Stripe's test mode and Stripe Elements on the frontend.
  2. 2Create the actual charge/subscription from your backend, never the frontend, using your secret key.
  3. 3Handle Stripe webhooks on the backend to confirm payment success reliably.

More topics at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge