Free tutorials & notes in Hindi & English · Clean code examples 🧰 मुफ़्त PDF और फ़ोटो टूल्स →
🛠️ PHP + MySQL REST API  ·  Lesson 03

API Setup: Folders, PDO और JSON

हमारी API कैसी दिखती है

REST API URLs का एक set है जो web pages की जगह data लौटाता है। App request भेजती है, PHP चलता है, और हम JSON में जवाब देते हैं:

Android appapi/product.phpMySQLGET /api/product.php?id=7SELECT … WHERE id = ?row200 {"success":true,"data":{…}}
एक request, एक JSON जवाब
Methodकिसके लिएExample
GETdata पढ़नाproducts.php?q=watch
POSTdata बनाना / बदलना (body JSON में)cart/add.php {"product_id":7,"qty":1}

हम इसे simple रखते हैं: सिर्फ GET और POST, हर endpoint के लिए एक PHP file। यह बिना किसी special server rule के हर hosting पर चलता है।

Folder structure

📁 backend/
  .htaccess
  database.sql
  📁 includes/            ← shared code, blocked from the browser
    .htaccess
    config.php         DB password, BASE_URL, delivery fee
    db.php             one PDO connection
    helpers.php        json_out(), input(), require_auth() …
    cart.php           cart totals + coupons (later lesson)
  📁 api/                 ← one PHP file = one endpoint
    index.php          health check
    home.php  products.php  product.php  categories.php  banners.php
    📁 auth/  cart/  wishlist/  address/  coupons/  orders/  reviews/
  📁 admin/               ← admin panel (later lesson)
  📁 uploads/             ← images (PHP disabled here)
backend/ — इस folder को hosting पर public_html/karteasy में upload करें

Step 1: config.php

सारी settings एक file में। हर value environment variable (getenv) से भी आ सकती है, जो local testing में काम आता है — hosting पर आप बस default values edit कर दें।

PHP – backend/includes/config.php
<?php
// ------------------------------------------------------------------
// KartEasy configuration. Edit these values after uploading.
// On Hostinger: hPanel -> Databases -> MySQL Databases shows the
// database name, user and host (usually "localhost").
// ------------------------------------------------------------------

define('DB_HOST', getenv('KE_DB_HOST') ?: 'localhost');
define('DB_PORT', getenv('KE_DB_PORT') ?: '3306');
define('DB_NAME', getenv('KE_DB_NAME') ?: 'karteasy');
define('DB_USER', getenv('KE_DB_USER') ?: 'karteasy_user');
define('DB_PASS', getenv('KE_DB_PASS') ?: 'change-this-password');

// Full URL of the backend folder, used to turn "uploads/x.png" into a full image URL.
// Example: https://yourdomain.com/karteasy
define('BASE_URL', rtrim(getenv('KE_BASE_URL') ?: 'https://yourdomain.com/karteasy', '/'));

define('TOKEN_DAYS', 30);          // login token validity
define('DELIVERY_FEE', 40);        // charged when subtotal is below FREE_DELIVERY_ABOVE
define('FREE_DELIVERY_ABOVE', 499);
define('DEBUG', getenv('KE_DEBUG') === '1'); // never true on a live site

date_default_timezone_set('Asia/Kolkata');
⚠️ Live site पर DEBUG off रखें

DEBUG on होने पर असली error messages (table names, SQL) app को भेजे जाते हैं। अपने computer पर काम के, online ख़तरनाक।

Step 2: db.php (PDO)

PDO PHP की database layer है। हम हर request में connection सिर्फ एक बार बनाते हैं (static $pdo) और तीन ज़रूरी options on करते हैं:

PHP – backend/includes/db.php
<?php
require_once __DIR__ . '/config.php';

/**
 * One shared PDO connection per request.
 * Prepared statements are real (not emulated) and errors throw exceptions.
 */
function db()
{
    static $pdo = null;
    if ($pdo === null) {
        $dsn = 'mysql:host=' . DB_HOST . ';port=' . DB_PORT . ';dbname=' . DB_NAME . ';charset=utf8mb4';
        $pdo = new PDO($dsn, DB_USER, DB_PASS, array(
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ));
    }
    return $pdo;
}
Optionक्या करता है
ERRMODE_EXCEPTIONSQL errors चुपचाप false लौटाने की जगह exception throw करते हैं
FETCH_ASSOCrows array('name' => 'Smart Watch Pro', …) की तरह आती हैं
EMULATE_PREPARES falseअसली prepared statements — SQL injection से मुख्य बचाव

Step 3: helpers.php

हर endpoint require_once __DIR__ . '/../includes/helpers.php'; से शुरू होता है। यह file JSON headers set करती है, सारे errors पकड़ती है और छोटे helper functions देती है। इसे एक बार ध्यान से पढ़ें — बाकी पूरी API इन्हीं टुकड़ों से बनी है।

PHP – backend/includes/helpers.php
<?php
require_once __DIR__ . '/db.php';

// Every API response is JSON, and the app may call us from any origin.
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;
}

// Any uncaught error becomes a clean JSON 500 instead of an HTML page.
set_exception_handler(function ($e) {
    error_log('[KartEasy] ' . $e->getMessage());
    $msg = DEBUG ? $e->getMessage() : 'Something went wrong. Please try again.';
    json_out(array('success' => false, 'message' => $msg), 500);
});

/** Send a JSON response and stop. */
function json_out($data, $code = 200)
{
    http_response_code($code);
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    exit;
}

function ok($data = null, $message = 'OK')
{
    json_out(array('success' => true, 'message' => $message, 'data' => $data));
}

function fail($message, $code = 400)
{
    json_out(array('success' => false, 'message' => $message, 'data' => null), $code);
}

/** Only allow the given HTTP method. */
function require_method($method)
{
    if ($_SERVER['REQUEST_METHOD'] !== $method) {
        fail('Method not allowed. Use ' . $method . '.', 405);
    }
}

/**
 * Read the request body. Works for JSON bodies (Retrofit @Body)
 * and for normal form posts (@FormUrlEncoded).
 */
function input()
{
    static $data = null;
    if ($data === null) {
        $raw = file_get_contents('php://input');
        $json = json_decode($raw, true);
        $data = is_array($json) ? $json : $_POST;
    }
    return $data;
}

function in_str($key, $default = '')
{
    $d = input();
    return isset($d[$key]) ? trim((string) $d[$key]) : $default;
}

function in_int($key, $default = 0)
{
    $d = input();
    return isset($d[$key]) ? (int) $d[$key] : $default;
}

function q_int($key, $default = 0)
{
    return isset($_GET[$key]) ? (int) $_GET[$key] : $default;
}

function q_str($key, $default = '')
{
    return isset($_GET[$key]) ? trim((string) $_GET[$key]) : $default;
}

/** Return "Name is required" style errors for missing fields. */
function require_fields($fields)
{
    foreach ($fields as $key => $label) {
        if (in_str($key) === '') {
            fail($label . ' is required.', 422);
        }
    }
}

/** "uploads/p1.png" -> "https://site.com/karteasy/uploads/p1.png" */
function img_url($path)
{
    if ($path === null || $path === '') {
        return null;
    }
    if (preg_match('#^https?://#', $path)) {
        return $path;
    }
    return BASE_URL . '/' . ltrim($path, '/');
}

/** Read the Bearer token from the Authorization header. */
function bearer_token()
{
    $h = '';
    if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
        $h = $_SERVER['HTTP_AUTHORIZATION'];
    } elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
        $h = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
    } elseif (function_exists('apache_request_headers')) {
        $all = apache_request_headers();
        foreach ($all as $k => $v) {
            if (strtolower($k) === 'authorization') {
                $h = $v;
            }
        }
    }
    if (preg_match('/Bearer\s+([a-f0-9]{64})/i', $h, $m)) {
        return strtolower($m[1]);
    }
    return null;
}

/** Create a new login token for a user. Only its sha256 hash is stored. */
function create_token($userId)
{
    $token = bin2hex(random_bytes(32));
    $stmt = db()->prepare('INSERT INTO user_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?)');
    $stmt->execute(array($userId, hash('sha256', $token), date('Y-m-d H:i:s', time() + TOKEN_DAYS * 86400)));
    return $token;
}

/** Returns the logged-in user row, or stops with 401. */
function require_auth()
{
    $token = bearer_token();
    if ($token === null) {
        fail('Please login to continue.', 401);
    }
    $stmt = db()->prepare(
        'SELECT u.id, u.name, u.email, u.phone, u.created_at FROM user_tokens t
         JOIN users u ON u.id = t.user_id
         WHERE t.token_hash = ? AND t.expires_at > NOW()'
    );
    $stmt->execute(array(hash('sha256', $token)));
    $user = $stmt->fetch();
    if (!$user) {
        fail('Session expired. Please login again.', 401);
    }
    $user['id'] = (int) $user['id'];
    return $user;
}

/** Shape one product row for the app (numbers as numbers, full image URL). */
function product_out($p)
{
    $price = (float) $p['price'];
    $mrp = (float) $p['mrp'];
    return array(
        'id'           => (int) $p['id'],
        'category_id'  => (int) $p['category_id'],
        'name'         => $p['name'],
        'short_desc'   => $p['short_desc'],
        'price'        => $price,
        'mrp'          => $mrp,
        'discount_pct' => $mrp > $price ? (int) round(($mrp - $price) * 100 / $mrp) : 0,
        'stock'        => (int) $p['stock'],
        'image'        => img_url($p['image']),
        'rating'       => (float) $p['rating_avg'],
        'rating_count' => (int) $p['rating_count'],
    );
}

/** Product ids in the user's wishlist, as a lookup array [id => true]. */
function wishlist_ids($userId)
{
    $stmt = db()->prepare('SELECT product_id FROM wishlist WHERE user_id = ?');
    $stmt->execute(array($userId));
    $ids = array();
    foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $id) {
        $ids[(int) $id] = true;
    }
    return $ids;
}

/** Optional login: returns the user id if a valid token was sent, else 0. */
function optional_user_id()
{
    $token = bearer_token();
    if ($token === null) {
        return 0;
    }
    $stmt = db()->prepare('SELECT user_id FROM user_tokens WHERE token_hash = ? AND expires_at > NOW()');
    $stmt->execute(array(hash('sha256', $token)));
    $id = $stmt->fetchColumn();
    return $id ? (int) $id : 0;
}

function money($n)
{
    return round((float) $n, 2);
}

/** Money for messages: ₹1,899 or ₹224.80 */
function rs($n)
{
    $n = round((float) $n, 2);
    return '₹' . number_format($n, floor($n) == $n ? 0 : 2);
}
Helperकाम
json_out / ok / failसही HTTP status के साथ JSON भेजें और रुक जाएँ
require_method('POST')गलत method पर 405
input(), in_str(), in_int()JSON body (या normal form post) पढ़ें
q_str(), q_int()?query parameters पढ़ें
require_fields()"Email is required." जैसे 422 errors
img_url()uploads/p7.png को Glide के लिए पूरे https URL में बदलें
require_auth(), create_token()login tokens (अगला lesson)
product_out()हर जगह product का एक जैसा JSON shape
PHP – helpers से समय क्यों बचता है
<?php
// Without helpers – every endpoint would repeat this:
header('Content-Type: application/json; charset=utf-8');
http_response_code(404);
echo json_encode(array('success' => false, 'message' => 'Product not found.', 'data' => null));
exit;

// With helpers – one line:
fail('Product not found.', 404);

हर चीज़ के लिए एक response format

हर endpoint — success हो या error — वही तीन keys लौटाता है। तब Android app सारी calls के लिए एक Kotlin class (ApiResponse<T>) use कर सकती है।

Success

  • HTTP 200
  • "success": true
  • "message": "Added to cart."
  • "data": { … }

Error

  • HTTP 401 / 404 / 409 / 422
  • "success": false
  • "message": "Only 12 left in stock."
  • "data": null
Message हमेशा पढ़ने लायक होता है: app उसे सीधे Snackbar में दिखाती है
StatusKartEasy में मतलब
200OK
401login नहीं / token expire
403allowed नहीं (जैसे delivery से पहले review)
404नहीं मिला
405गलत method (POST की जगह GET)
409conflict: stock ख़त्म, email पहले से use
422validation error: गलत phone, field missing
500server bug (details सिर्फ error log में)

Step 4: Private files बचाएँ

तीन छोटी .htaccess files (Apache / LiteSpeed — Hostinger और cPanel पर यही होते हैं):

Apache – backend/.htaccess
Options -Indexes

# Pass the "Authorization: Bearer ..." header to PHP (some Apache/LiteSpeed setups drop it).
RewriteEngine On
RewriteCond %{HTTP:Authorization} .
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

# The SQL file must never be downloadable.
<Files "database.sql">
  <IfModule mod_authz_core.c>
    Require all denied
  </IfModule>
  <IfModule !mod_authz_core.c>
    Deny from all
  </IfModule>
</Files>
Apache – backend/includes/.htaccess
# Nobody may open these files from a browser (they contain the DB password).
<IfModule mod_authz_core.c>
  Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
  Deny from all
</IfModule>
Apache – backend/uploads/.htaccess
# Uploaded files are images only: never run PHP from this folder.
<FilesMatch "\.(php|phtml|php3|php4|php5|php7|phar)$">
  <IfModule mod_authz_core.c>
    Require all denied
  </IfModule>
  <IfModule !mod_authz_core.c>
    Deny from all
  </IfModule>
</FilesMatch>
Options -Indexes
🔑 Authorization वाला rule क्यों?

कुछ shared hosts पर PHP को Authorization: Bearer … header दिखता ही नहीं। RewriteRule उसे $_SERVER['HTTP_AUTHORIZATION'] में copy करता है, और bearer_token() तीनों जगह check करता है।

Step 5: पहला endpoint + चलाएँ

"Health check" endpoint एक नज़र में बता देता है कि PHP, config और database ठीक हैं:

PHP – backend/api/index.php
<?php
// Open https://yourdomain.com/karteasy/api/ in a browser to check that the API and database work.
require_once __DIR__ . '/../includes/helpers.php';

$products = (int) db()->query('SELECT COUNT(*) FROM products')->fetchColumn();
ok(array(
    'name' => 'KartEasy API',
    'version' => '1.0',
    'database' => 'connected',
    'products' => $products,
    'php' => PHP_VERSION,
    'endpoints' => array(
        'GET  home.php', 'GET  categories.php', 'GET  banners.php', 'GET  products.php', 'GET  product.php?id=',
        'POST auth/register.php', 'POST auth/login.php', 'POST auth/logout.php', 'GET  auth/me.php', 'POST auth/update.php',
        'GET  cart/list.php', 'POST cart/add.php', 'POST cart/update.php', 'POST cart/remove.php',
        'GET  wishlist/list.php', 'POST wishlist/toggle.php',
        'GET  address/list.php', 'POST address/save.php', 'POST address/delete.php',
        'GET  coupons/list.php', 'POST coupons/apply.php',
        'POST orders/place.php', 'GET  orders/list.php', 'GET  orders/detail.php?id=', 'POST orders/cancel.php',
        'GET  reviews/list.php?product_id=', 'POST reviews/add.php',
    ),
), 'API is running');

Backend को PHP के built-in server से चलाएँ (XAMPP Apache की ज़रूरत नहीं):

Terminal
cd backend

# Use your own local DB user. The env vars override config.php (no file edit needed)
KE_DB_USER=root KE_DB_PASS= KE_BASE_URL=http://10.0.2.2:8000 KE_DEBUG=1 php -S 0.0.0.0:8000

# New terminal window:
curl http://localhost:8000/api/
असली response
GET /karteasy/api/ HTTP 200 { "success": true, "message": "API is running", "data": { "name": "KartEasy API", "version": "1.0", "database": "connected", "products": 16, "php": "8.3.6", "endpoints": [ "GET home.php", "GET categories.php", "GET banners.php", "GET products.php", "GET product.php?id=", "POST auth/register.php", "… 21 more" ] } }
🔒 localhost:8000/api/{ "success": true, "message": "API is running", "data": { "name": "KartEasy API", "database": "connected", "products": 16, "php": "8.3.6", "endpoints": [ … ] } }
❌ Error दिख रहा है?

HTTP 500 के साथ "Something went wrong" का मतलब ज़्यादातर गलत DB_USER / DB_PASS / DB_NAME है। असली message देखने के लिए KE_DEBUG=1 से शुरू करें। Hostinger पर host localhost होता है और names में u123456_ जैसा prefix।

PHP 7.2 friendly code

बहुत-सी shared hosting पर अभी भी पुराना PHP है। पूरे backend को PHPCompatibility से PHP 7.2+ के लिए check किया गया है, इसलिए यह 7.2, 7.4, 8.0 … 8.3 पर चलता है। हम नया syntax avoid करते हैं:

Avoid किया (PHP 7.4 / 8 चाहिए)

  • fn($x) => $x * 2
  • private int $count;
  • match ($status) { … }
  • $user?->name
  • str_contains()

इसकी जगह (PHP 7.2 OK)

  • function ($x) { return $x * 2; }
  • private $count;
  • if / elseif या array map
  • isset($user) ? $user->name : null
  • strpos($a, $b) !== false
Result वही, hosting support ज़्यादा

अक्सर पूछे जाने वाले प्रश्न (FAQ)

Laravel की जगह plain PHP files क्यों?
Plain PHP हर सस्ती shared hosting पर बिना Composer या terminal के चलता है, और हर line साफ़ दिखती है। यही ideas (routes, validation, JSON, tokens) बाद में Laravel में सीधे लागू होते हैं।
ATTR_EMULATE_PREPARES = false क्यों?
इससे MySQL असली prepared statements चलाता है, values हमेशा SQL से अलग भेजी जाती हैं। Integers भी integers ही लौटते हैं।
Android app के लिए Access-Control-Allow-Origin क्या करता है?
कुछ नहीं — Android CORS ignore करता है। यह इसलिए है ताकि बाद में इसी API को website या React/Next.js app से भी call कर सकें।
← E-commerce App Tutorial
🔗

Share this topic with a friend

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

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

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

अगला topicRegister और Login API (Token) आपकी प्रगति: 3 / 28 (11%) · Lesson 4 / 28 आगे बढ़ें →

यह lesson कैसा लगा?

आपकी राय से हम content और बेहतर बनाते हैं।

अपना review और सुझाव लिखें (वैकल्पिक)

Review approve होने के बाद आपका नाम और शहर इसके साथ public दिख सकते हैं। Email या phone न लिखें।

▶ CodingEasily on YouTube

Video से सीखें – आसान Hindi में

चुनी हुई video: RDBMS भाग 3 – Class XI Commerce IT

▶ Channel subscribe करें