E-commerce API Example

Complete e-commerce backend with MoroJS. Features product catalog, shopping cart, payment processing, order management, and inventory tracking.

E-commerce Features

Shopping Cart

  • • Add/remove items
  • • Quantity management
  • • Price calculations
  • • Cart persistence

Payment Processing

  • • Stripe integration
  • • Secure transactions
  • • Payment webhooks
  • • Refund handling

Order Management

  • • Order tracking
  • • Inventory management
  • • Shipping integration
  • • Order history

E-commerce API Implementation

Product and Cart APIs

typescript

1// Product catalog endpoints
2app.get('/products', {
3  query: z.object({
4    category: z.string().optional(),
5    search: z.string().optional(),
6    minPrice: z.coerce.number().optional(),
7    maxPrice: z.coerce.number().optional(),
8    inStock: z.boolean().optional(),
9    page: z.coerce.number().default(1),
10    limit: z.coerce.number().max(50).default(20)
11  }),
12  handler: async ({ query, db }) => {
13    const products = await db.query(`
14      SELECT p.*, c.name as category_name, i.quantity as stock_quantity
15      FROM products p
16      LEFT JOIN categories c ON p.category_id = c.id
17      LEFT JOIN inventory i ON p.id = i.product_id
18      WHERE ($1::text IS NULL OR c.name ILIKE $1)
19        AND ($2::text IS NULL OR p.name ILIKE $2 OR p.description ILIKE $2)
20        AND ($3::numeric IS NULL OR p.price >= $3)
21        AND ($4::numeric IS NULL OR p.price <= $4)
22        AND ($5::boolean IS NULL OR ($5 = true AND i.quantity > 0))
23      ORDER BY p.created_at DESC
24      LIMIT $6 OFFSET $7
25    `, [
26      query.category ? `%${query.category}%` : null,
27      query.search ? `%${query.search}%` : null,
28      query.minPrice,
29      query.maxPrice,
30      query.inStock,
31      query.limit,
32      (query.page - 1) * query.limit
33    ]);
34    
35    return products;
36  }
37});
38
39// Shopping cart endpoints
40app.get('/cart', {
41  middleware: [requireAuth],
42  handler: async ({ context, db }) => {
43    const cartItems = await db.query(`
44      SELECT ci.*, p.name, p.price, p.image_url
45      FROM cart_items ci
46      JOIN products p ON ci.product_id = p.id
47      WHERE ci.user_id = $1
48    `, [context.user.id]);
49    
50    const total = cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0);
51    
52    return {
53      items: cartItems,
54      total,
55      itemCount: cartItems.length
56    };
57  }
58});
59
60app.post('/cart/items', {
61  middleware: [requireAuth],
62  body: z.object({
63    productId: z.string().uuid(),
64    quantity: z.number().positive().max(10)
65  }),
66  handler: async ({ body, context, db }) => {
67    // Check product availability
68    const product = await db.query(
69      'SELECT id, price FROM products WHERE id = $1',
70      [body.productId]
71    );
72    
73    if (!product[0]) {
74      throw new Error('Product not found');
75    }
76    
77    // Check inventory
78    const inventory = await db.query(
79      'SELECT quantity FROM inventory WHERE product_id = $1',
80      [body.productId]
81    );
82    
83    if (!inventory[0] || inventory[0].quantity < body.quantity) {
84      throw new Error('Insufficient inventory');
85    }
86    
87    // Add to cart (or update existing)
88    const cartItem = await db.query(`
89      INSERT INTO cart_items (user_id, product_id, quantity)
90      VALUES ($1, $2, $3)
91      ON CONFLICT (user_id, product_id)
92      DO UPDATE SET quantity = cart_items.quantity + $3
93      RETURNING *
94    `, [context.user.id, body.productId, body.quantity]);
95    
96    return cartItem[0];
97  }
98});
99
100// Checkout process
101app.post('/checkout', {
102  middleware: [requireAuth],
103  body: z.object({
104    shippingAddress: z.object({
105      street: z.string(),
106      city: z.string(),
107      state: z.string(),
108      zipCode: z.string(),
109      country: z.string()
110    }),
111    paymentMethod: z.string(), // Stripe payment method ID
112    couponCode: z.string().optional()
113  }),
114  handler: async ({ body, context, db }) => {
115    return await db.transaction(async (tx) => {
116      // Get cart items
117      const cartItems = await tx.query(
118        'SELECT ci.*, p.price FROM cart_items ci JOIN products p ON ci.product_id = p.id WHERE ci.user_id = $1',
119        [context.user.id]
120      );
121      
122      if (cartItems.length === 0) {
123        throw new Error('Cart is empty');
124      }
125      
126      // Calculate totals
127      let subtotal = cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0);
128      let discount = 0;
129      
130      // Apply coupon if provided
131      if (body.couponCode) {
132        const coupon = await tx.query(
133          'SELECT * FROM coupons WHERE code = $1 AND active = true AND expires_at > NOW()',
134          [body.couponCode]
135        );
136        
137        if (coupon[0]) {
138          discount = coupon[0].type === 'percentage' 
139            ? subtotal * (coupon[0].value / 100)
140            : coupon[0].value;
141        }
142      }
143      
144      const total = subtotal - discount;
145      
146      // Create order
147      const order = await tx.query(`
148        INSERT INTO orders (user_id, subtotal, discount, total, status, shipping_address)
149        VALUES ($1, $2, $3, $4, 'pending', $5)
150        RETURNING *
151      `, [context.user.id, subtotal, discount, total, JSON.stringify(body.shippingAddress)]);
152      
153      // Create order items
154      for (const item of cartItems) {
155        await tx.query(`
156          INSERT INTO order_items (order_id, product_id, quantity, price)
157          VALUES ($1, $2, $3, $4)
158        `, [order[0].id, item.product_id, item.quantity, item.price]);
159        
160        // Update inventory
161        await tx.query(
162          'UPDATE inventory SET quantity = quantity - $1 WHERE product_id = $2',
163          [item.quantity, item.product_id]
164        );
165      }
166      
167      // Process payment
168      const paymentResult = await processPayment({
169        amount: total,
170        currency: 'usd',
171        paymentMethod: body.paymentMethod,
172        orderId: order[0].id
173      });
174      
175      if (!paymentResult.success) {
176        throw new Error('Payment failed');
177      }
178      
179      // Update order status
180      await tx.query(
181        'UPDATE orders SET status = $1, payment_id = $2 WHERE id = $3',
182        ['paid', paymentResult.paymentId, order[0].id]
183      );
184      
185      // Clear cart
186      await tx.query('DELETE FROM cart_items WHERE user_id = $1', [context.user.id]);
187      
188      // Emit order created event
189      events.emit('order.created', { order: order[0], items: cartItems });
190      
191      return {
192        order: order[0],
193        payment: paymentResult
194      };
195    });
196  }
197});

Run the Example

Getting Started

typescript

1# Clone and setup
2git clone https://github.com/Moro-JS/examples.git
3cd examples/ecommerce-api
4
5# Install dependencies
6npm install
7
8# Setup environment
9cp .env.example .env
10# Add your Stripe keys and database URL
11
12# Setup database
13npm run db:setup
14npm run db:migrate
15npm run db:seed  # Load sample products
16
17# Start development
18npm run dev
19
20# Test the API:
21curl http://localhost:3000/products
22curl http://localhost:3000/categories
23
24# Available scripts:
25npm run dev          # Development server
26npm run build        # Build for production
27npm run start        # Production server
28npm run test         # Run tests
29npm run db:migrate   # Database migrations
30npm run db:seed      # Seed sample data

What You'll Learn

  • • E-commerce data modeling
  • • Payment processing with Stripe
  • • Inventory management
  • • Order workflow design
  • • Transaction handling
  • • Webhook processing

Next Steps