Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
🛠️ PHP + MySQL REST API  ·  Lesson 08

Wishlist & Address API

Wishlist: one toggle endpoint

tap ♡POST wishlist/toggle.phpDELETE rowdeleted 1 row?yes → removedin_wishlist: falseno → INSERTin_wishlist: true
Try to delete first; if nothing was deleted, it was not saved yet, so insert

Step 1: wishlist/toggle.php and list.php

rowCount() tells how many rows the DELETE removed. That single number tells us whether the product was in the wishlist. The UNIQUE (user_id, product_id) key prevents duplicates even if the user double-taps.

PHP – backend/api/wishlist/toggle.php
<?php
// POST {product_id} -> adds if missing, removes if present
require_once __DIR__ . '/../../includes/helpers.php';
require_method('POST');
$user = require_auth();
$productId = in_int('product_id');

$chk = db()->prepare('SELECT id FROM products WHERE id = ? AND is_active = 1');
$chk->execute(array($productId));
if (!$chk->fetch()) {
    fail('Product not found.', 404);
}

$del = db()->prepare('DELETE FROM wishlist WHERE user_id = ? AND product_id = ?');
$del->execute(array($user['id'], $productId));
if ($del->rowCount() > 0) {
    ok(array('product_id' => $productId, 'in_wishlist' => false), 'Removed from wishlist.');
}
db()->prepare('INSERT INTO wishlist (user_id, product_id) VALUES (?, ?)')->execute(array($user['id'], $productId));
ok(array('product_id' => $productId, 'in_wishlist' => true), 'Added to wishlist.');
PHP – backend/api/wishlist/list.php
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_method('GET');
$user = require_auth();

$stmt = db()->prepare(
    'SELECT p.* FROM wishlist w JOIN products p ON p.id = w.product_id
     WHERE w.user_id = ? AND p.is_active = 1 ORDER BY w.id DESC'
);
$stmt->execute(array($user['id']));
$out = array();
foreach ($stmt->fetchAll() as $r) {
    $p = product_out($r);
    $p['in_wishlist'] = true;
    $out[] = $p;
}
ok($out);

Addresses: what checkout needs

9:41▮▮ 85%←Saved addresses⋮Riya Sharma · 98765 …12 MG Road, Indore – 45…DefaultEditDeleteOffice · 98765 000004th Floor, Tech Park, I…+ Add address9:41▮▮ 85%←Add address⋮Full name+91 Mobile numberHouse no, building, streetCity PincodeState ▾Make this my default addressSave address
The address screens use address/list.php and address/save.php

Rules: Indian mobile (10 digits, starts with 6–9), pincode (6 digits, not starting with 0), and exactly one default address. The first address is always default.

Step 2: address/save.php

One endpoint for both "add" (no id) and "edit" (with id). Making an address default means first clearing the old default — two queries that must happen together, so they run inside a transaction.

PHP – backend/api/address/save.php
<?php
// POST {id?, name, phone, line1, line2?, city, state, pincode, is_default?}
// Without id -> new address. With id -> update that address.
require_once __DIR__ . '/../../includes/helpers.php';
require_once __DIR__ . '/../../includes/address.php';
require_method('POST');
$user = require_auth();
require_fields(array(
    'name' => 'Name', 'phone' => 'Phone', 'line1' => 'Address line 1',
    'city' => 'City', 'state' => 'State', 'pincode' => 'Pincode',
));

$id = in_int('id');
$phone = in_str('phone');
$pincode = in_str('pincode');
if (!preg_match('/^[6-9][0-9]{9}$/', $phone)) {
    fail('Please enter a valid 10-digit mobile number.', 422);
}
if (!preg_match('/^[1-9][0-9]{5}$/', $pincode)) {
    fail('Please enter a valid 6-digit pincode.', 422);
}

$countStmt = db()->prepare('SELECT COUNT(*) FROM addresses WHERE user_id = ?');
$countStmt->execute(array($user['id']));
$isFirst = (int) $countStmt->fetchColumn() === 0;
$makeDefault = $isFirst || in_int('is_default') === 1 || in_str('is_default') === 'true';

$pdo = db();
$pdo->beginTransaction();
if ($makeDefault) {
    $pdo->prepare('UPDATE addresses SET is_default = 0 WHERE user_id = ?')->execute(array($user['id']));
}
$line2 = in_str('line2');
$values = array(in_str('name'), $phone, in_str('line1'), $line2 === '' ? null : $line2, in_str('city'), in_str('state'), $pincode);

if ($id > 0) {
    $own = $pdo->prepare('SELECT id FROM addresses WHERE id = ? AND user_id = ?');
    $own->execute(array($id, $user['id']));
    if (!$own->fetch()) {
        $pdo->rollBack();
        fail('Address not found.', 404);
    }
    $sql = 'UPDATE addresses SET name = ?, phone = ?, line1 = ?, line2 = ?, city = ?, state = ?, pincode = ?'
         . ($makeDefault ? ', is_default = 1' : '') . ' WHERE id = ? AND user_id = ?';
    $values[] = $id;
    $values[] = $user['id'];
    $pdo->prepare($sql)->execute($values);
} else {
    $values[] = $makeDefault ? 1 : 0;
    $values[] = $user['id'];
    $pdo->prepare(
        'INSERT INTO addresses (name, phone, line1, line2, city, state, pincode, is_default, user_id)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
    )->execute($values);
    $id = (int) $pdo->lastInsertId();
}
$pdo->commit();

$stmt = db()->prepare('SELECT * FROM addresses WHERE id = ?');
$stmt->execute(array($id));
ok(address_out($stmt->fetch()), 'Address saved.');
PHP – backend/includes/address.php
<?php
function address_out($a)
{
    return array(
        'id' => (int) $a['id'],
        'name' => $a['name'],
        'phone' => $a['phone'],
        'line1' => $a['line1'],
        'line2' => $a['line2'],
        'city' => $a['city'],
        'state' => $a['state'],
        'pincode' => $a['pincode'],
        'is_default' => (int) $a['is_default'] === 1,
    );
}

function address_text($a)
{
    $parts = array($a['line1']);
    if ($a['line2'] !== null && $a['line2'] !== '') {
        $parts[] = $a['line2'];
    }
    $parts[] = $a['city'] . ', ' . $a['state'] . ' - ' . $a['pincode'];
    return implode(', ', $parts);
}

Ownership checks (very important)

The most common real-world API bug is called IDOR (Insecure Direct Object Reference): trusting an id sent by the app without checking who owns it.

PHP
<?php
// ❌ Dangerous: deletes ANY address whose id is sent
db()->prepare('DELETE FROM addresses WHERE id = ?')->execute(array($id));
// User 1 sends {"id": 57} → deletes user 2's address!

// ✅ Safe: the row must belong to the logged-in user
db()->prepare('DELETE FROM addresses WHERE id = ? AND user_id = ?')->execute(array($id, $user['id']));
❌ Every query with an id needs "AND user_id = ?"

KartEasy does this for addresses, cart rows, wishlist, orders, order details and cancel. Search the backend for user_id = ? — you will find it in every customer endpoint.

Step 3: list.php and delete.php

When the default address is deleted, the newest remaining one becomes default, so checkout always has an address to pre-select.

PHP – backend/api/address/list.php
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_once __DIR__ . '/../../includes/address.php';
require_method('GET');
$user = require_auth();

$stmt = db()->prepare('SELECT * FROM addresses WHERE user_id = ? ORDER BY is_default DESC, id DESC');
$stmt->execute(array($user['id']));
$out = array();
foreach ($stmt->fetchAll() as $a) {
    $out[] = address_out($a);
}
ok($out);
PHP – backend/api/address/delete.php
<?php
// POST {id}
require_once __DIR__ . '/../../includes/helpers.php';
require_method('POST');
$user = require_auth();
$id = in_int('id');

$stmt = db()->prepare('SELECT is_default FROM addresses WHERE id = ? AND user_id = ?');
$stmt->execute(array($id, $user['id']));
$row = $stmt->fetch();
if (!$row) {
    fail('Address not found.', 404);
}
db()->prepare('DELETE FROM addresses WHERE id = ?')->execute(array($id));

// If the default was deleted, make the newest remaining address the default.
if ((int) $row['is_default'] === 1) {
    db()->prepare('UPDATE addresses SET is_default = 1 WHERE user_id = ? ORDER BY id DESC LIMIT 1')
        ->execute(array($user['id']));
}
ok(null, 'Address deleted.');

Real responses

Toggle the heart
POST /karteasy/api/wishlist/toggle.php Authorization: Bearer 3f9c…e1a7 {"product_id": 4} HTTP 200 { "success": true, "message": "Added to wishlist.", "data": { "product_id": 4, "in_wishlist": true } }
Wishlist
GET /karteasy/api/wishlist/list.php Authorization: Bearer 3f9c…e1a7 HTTP 200 { "success": true, "message": "OK", "data": [ { "id": 4, "category_id": 2, "name": "Men Running Sneakers", "short_desc": "Lightweight mesh, sizes 6–11", "price": 1299, "mrp": 2499, "discount_pct": 48, "stock": 60, "image": "https://yourdomain.com/karteasy/uploads/products/p4.png", "rating": 4.2, "rating_count": 330, "in_wishlist": true } ] }
Wrong phone number
POST /karteasy/api/address/save.php Authorization: Bearer 3f9c…e1a7 {"name": "Riya Sharma", "phone": "12345", "line1": "12 MG Road", "city": "Indore", "state": "Madhya Pradesh", "pincode": "452001"} HTTP 422 { "success": false, "message": "Please enter a valid 10-digit mobile number.", "data": null }
Save the first address (becomes default)
POST /karteasy/api/address/save.php Authorization: Bearer 3f9c…e1a7 {"name": "Riya Sharma", "phone": "9876543210", "line1": "12 MG Road", "line2": "Near City Mall", "city": "Indore", "state": "Madhya Pradesh", "pincode": "452001"} HTTP 200 { "success": true, "message": "Address saved.", "data": { "id": 1, "name": "Riya Sharma", "phone": "9876543210", "line1": "12 MG Road", "line2": "Near City Mall", "city": "Indore", "state": "Madhya Pradesh", "pincode": "452001", "is_default": true } }
Address list
GET /karteasy/api/address/list.php Authorization: Bearer 3f9c…e1a7 HTTP 200 { "success": true, "message": "OK", "data": [ { "id": 1, "name": "Riya Sharma", "phone": "9876543210", "line1": "12 MG Road", "line2": "Near City Mall", "city": "Indore", "state": "Madhya Pradesh", "pincode": "452001", "is_default": true } ] }

Frequently Asked Questions

Why toggle instead of separate add and remove?
The heart icon only knows "tap". One endpoint that flips the state and returns the new in_wishlist value keeps the app code tiny and can never get out of sync.
Can I validate the pincode with a real database?
Yes. India Post publishes the pincode list; you can import it into a table and check city/state, or call a pincode API from PHP. The regex here only checks the format.
← E-commerce App Tutorial
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicOrders & Checkout API Your progress: 8 / 28 (29%) · Lesson 9 / 28 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free