diff --git a/community/projects.mdx b/community/projects.mdx index 97b226e4..370af8ff 100644 --- a/community/projects.mdx +++ b/community/projects.mdx @@ -56,6 +56,11 @@ Browse community projects by category. Each card links directly to the repositor + + Starter template integrating Dodo Payments with Express, MongoDB, and verified webhook handling. +
Ideal for beginners and intermediate developers. +

`Express` `MongoDB` `TypeScript` +
Starter boilerplate for integrating Dodo Payments quickly.
Includes example checkout flow, API wiring, and environment configuration. diff --git a/developer-resources/express-mongodb-boilerplate.mdx b/developer-resources/express-mongodb-boilerplate.mdx new file mode 100644 index 00000000..70424c65 --- /dev/null +++ b/developer-resources/express-mongodb-boilerplate.mdx @@ -0,0 +1,390 @@ +--- +title: Express + MongoDB Boilerplate (Dodo Payments) +description: Production-ready starter to integrate Dodo Payments with Express and MongoDB, including webhooks and storage. +icon: "server" +--- + + +Prefer starting from the ready-to-use template repository: yashranaway/express-dodo + + + + + Install official SDKs for Node, Python, and Go. + + + + Learn webhook delivery, retries, and verification. + + + Fork and start coding instantly. + + + +## Overview + +This boilerplate shows how to build an Express server with MongoDB and integrate Dodo Payments end-to-end: +- Create subscriptions and one-time payments +- Verify and handle webhooks using `dodopayments-webhooks` +- Persist customers, payments, and subscription states in MongoDB +- Expose secure endpoints with environment-driven configuration + +## Prerequisites + +- Node.js 18+ +- MongoDB instance (Atlas or local) +- Dodo Payments API Key and Webhook Secret + +## Project Structure + +```bash +express-dodo/ + .env + src/ + app.ts + routes/ + payments.ts + subscriptions.ts + webhooks.ts + db/ + client.ts + models.ts + package.json + README.md +``` + +## Quickstart + +```bash +npm init -y +npm install express mongoose cors dotenv dodopayments dodopayments-webhooks +npm install -D typescript ts-node @types/express @types/node @types/cors +npx tsc --init +``` + + +TypeScript default imports (e.g., `import express from 'express'`) require enabling interop flags. After running `npx tsc --init`, update `tsconfig.json`: + +```json +{ + "compilerOptions": { + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + } +} +``` + +This guide uses default imports and assumes these flags are enabled. + + +`.env`: + +```bash +DODO_PAYMENTS_API_KEY=sk_test_xxx +DODO_WEBHOOK_SECRET=whsec_xxx +MONGODB_URI=mongodb+srv://:@/dodo?retryWrites=true&w=majority +PORT=3000 +``` + +## Database Setup + +```ts +// src/db/client.ts +import mongoose from 'mongoose'; + +export async function connectDB(uri: string) { + if (mongoose.connection.readyState === 1) return; + await mongoose.connect(uri, { dbName: 'dodo' }); +} +``` + +```ts +// src/db/models.ts +import mongoose from 'mongoose'; + +const CustomerSchema = new mongoose.Schema({ + customerId: { type: String, index: true }, + email: String, + name: String, +}); + +const PaymentSchema = new mongoose.Schema({ + paymentId: { type: String, index: true }, + status: String, + amount: Number, + currency: String, + customerId: String, + metadata: {}, +}); + +const SubscriptionSchema = new mongoose.Schema({ + subscriptionId: { type: String, index: true }, + status: String, + productId: String, + customerId: String, + currentPeriodEnd: Date, + metadata: {}, +}); + +export const Customer = mongoose.model('Customer', CustomerSchema); +export const Payment = mongoose.model('Payment', PaymentSchema); +export const Subscription = mongoose.model('Subscription', SubscriptionSchema); +``` + +## Express App + +```ts +// src/app.ts +import 'dotenv/config'; +import express from 'express'; +import cors from 'cors'; +import { connectDB } from './db/client'; +import paymentsRouter from './routes/payments'; +import subsRouter from './routes/subscriptions'; +import webhooksRouter from './routes/webhooks'; + +const app = express(); +app.use(cors()); +// Parse JSON for API routes; webhook routes MUST use raw body middleware +// (not express.json()) so the original bytes are preserved for signature +// verification. For security, apply raw body parsing only on the webhook path. +app.use('/api', express.json()); + +async function bootstrap() { + if (process.env.MONGODB_URI) { + await connectDB(process.env.MONGODB_URI); + } + + app.use('/api/payments', paymentsRouter); + app.use('/api/subscriptions', subsRouter); + app.use('/webhooks/dodo', webhooksRouter); + + const port = Number(process.env.PORT) || 3000; + app.listen(port, () => console.log(`Server listening on :${port}`)); +} + +bootstrap(); +``` + + +Alternative (no tsconfig changes): If you prefer not to enable `esModuleInterop`, switch to namespace/compat imports and Router construction: + +```ts +// src/app.ts (no esModuleInterop) +import * as express from 'express'; +import * as cors from 'cors'; + +const app = express(); +app.use(cors()); +``` + +```ts +// src/routes/payments.ts (no esModuleInterop) +import * as express from 'express'; +import DodoPayments from 'dodopayments'; + +const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY }); +const router = express.Router(); +``` + +Both approaches are valid; the rest of this guide uses default imports with `esModuleInterop` enabled. + + +## Payments Route + +```ts +// src/routes/payments.ts +import { Router } from 'express'; +import DodoPayments from 'dodopayments'; + +const router = Router(); + +router.post('/', async (req, res) => { + try { + const apiKey = process.env.DODO_PAYMENTS_API_KEY; + if (!apiKey) return res.status(500).json({ error: 'DODO_PAYMENTS_API_KEY is not set' }); + const client = new DodoPayments({ bearerToken: apiKey }); + const { + billing, + customer, + product_cart, + return_url, + metadata, + allowed_payment_method_types, + discount_code, + show_saved_payment_methods, + tax_id, + } = req.body; + + if (!billing || !customer || !product_cart) { + return res.status(400).json({ error: 'billing, customer, and product_cart are required' }); + } + + const payment = await client.payments.create({ + billing, + customer, + product_cart, + payment_link: true, + return_url, + metadata, + allowed_payment_method_types, + discount_code, + show_saved_payment_methods, + tax_id, + }); + + res.json(payment); + } catch (err: any) { + res.status(400).json({ error: err.message }); + } +}); + +export default router; +``` + +## Subscriptions Route + +```ts +// src/routes/subscriptions.ts +import { Router } from 'express'; +import DodoPayments from 'dodopayments'; + +const router = Router(); + +router.post('/', async (req, res) => { + try { + const apiKey = process.env.DODO_PAYMENTS_API_KEY; + if (!apiKey) return res.status(500).json({ error: 'DODO_PAYMENTS_API_KEY is not set' }); + const client = new DodoPayments({ bearerToken: apiKey }); + const { + billing, + customer, + product_id, + quantity, + return_url, + metadata, + discount_code, + show_saved_payment_methods, + tax_id, + trial_period_days, + } = req.body; + + if (!billing || !customer || !product_id || !quantity) { + return res.status(400).json({ error: 'billing, customer, product_id, and quantity are required' }); + } + + const sub = await client.subscriptions.create({ + billing, + customer, + product_id, + quantity, + payment_link: true, + return_url, + metadata, + discount_code, + show_saved_payment_methods, + tax_id, + trial_period_days, + }); + + res.json(sub); + } catch (err: any) { + res.status(400).json({ error: err.message }); + } +}); + +export default router; +``` + +## Webhooks (Verified) + +```ts +// src/routes/webhooks.ts +import { Router } from 'express'; +import express from 'express'; +import { Subscription, Payment } from '../db/models'; +import { DodopaymentsHandler } from 'dodopayments-webhooks'; + +const router = Router(); + +router.post('/', express.json(), async (req, res) => { + try { + const secret = process.env.DODO_WEBHOOK_SECRET; + if (!secret) return res.status(500).json({ error: 'DODO_WEBHOOK_SECRET is not set' }); + const handler = new DodopaymentsHandler({ signing_key: secret }); + const event = await handler.handle(req); + + switch (event.type) { + case 'subscription.active': { + const data = event.data as any; + await Subscription.updateOne( + { subscriptionId: data.subscription_id }, + { + subscriptionId: data.subscription_id, + status: 'active', + productId: data.product_id, + customerId: data.customer?.customer_id, + currentPeriodEnd: data.current_period_end ? new Date(data.current_period_end) : undefined, + metadata: data.metadata || {}, + }, + { upsert: true } + ); + break; + } + case 'subscription.on_hold': { + const data = event.data as any; + await Subscription.updateOne( + { subscriptionId: data.subscription_id }, + { status: 'on_hold' } + ); + break; + } + case 'payment.succeeded': { + const p = event.data as any; + await Payment.updateOne( + { paymentId: p.payment_id }, + { + paymentId: p.payment_id, + status: 'succeeded', + amount: p.total_amount, + currency: p.currency, + customerId: p.customer?.customer_id, + metadata: p.metadata || {}, + }, + { upsert: true } + ); + break; + } + case 'payment.failed': { + const p = event.data as any; + await Payment.updateOne( + { paymentId: p.payment_id }, + { status: 'failed' } + ); + break; + } + default: + break; + } + + return res.json({ received: true }); + } catch (err: any) { + return res.status(500).json({ error: err.message }); + } +}); + +export default router; +``` + + +Prefer using the community `dodopayments-webhooks` library for verified parsing and strong types. This library requires the raw request body to be preserved for signature verification — use a raw body middleware on the webhook route (or configure `express.json({ verify })` to capture the raw buffer) so verification can succeed. With raw-body-preserving middleware in place, the library enables verified parsing and strong types as advertised. See the repo: dodopayments-webhooks. + + +## Deploy Notes + +- Use environment variables for secrets +- Prefer HTTPS for webhook endpoint +- Configure retry-safe handlers (idempotent writes) +- Add indexes on `paymentId`, `subscriptionId`, and `customerId` + + diff --git a/docs.json b/docs.json index 14c63dd5..e06bedda 100644 --- a/docs.json +++ b/docs.json @@ -246,13 +246,14 @@ { "group": "Integration Guides", "pages": [ - "developer-resources/integration-guide", - "developer-resources/subscription-integration-guide", - "developer-resources/usage-based-billing-guide", - "developer-resources/checkout-session", - "developer-resources/subscription-upgrade-downgrade", - "developer-resources/overlay-checkout", - "developer-resources/mobile-integration" + "developer-resources/integration-guide", + "developer-resources/subscription-integration-guide", + "developer-resources/usage-based-billing-guide", + "developer-resources/checkout-session", + "developer-resources/express-mongodb-boilerplate", + "developer-resources/subscription-upgrade-downgrade", + "developer-resources/overlay-checkout", + "developer-resources/mobile-integration" ] },