Ravindra BagaleCourses & study guides

16. Live Project: Building a Reels App with EC2, S3 and RDS

16.8 The Feed API and Likes

feed.php JSON deto – 5 posts ek veli, navin aadhi. Pagination sathi OFFSET nahi tar cursor (?before=<last id>) vaparla aahe – fast aani scroll kartana duplicate posts yet nahit. Pratyek video sathi 20 minutancha presigned URL banto.

public/feed.php

<?php
// public/feed.php – JSON feed, newest first, cursor pagination, presigned S3 URLs
require __DIR__ . '/../src/bootstrap.php';

$user   = require_login(true);
$before = filter_input(INPUT_GET, 'before', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
$limit  = 5;

$sql = 'SELECT p.id, p.post_type, p.caption, p.body_text, p.bg_color, p.s3_key, p.mime_type,
               p.created_at, u.username,
               (SELECT COUNT(*) FROM likes l WHERE l.post_id = p.id) AS likes,
               EXISTS(SELECT 1 FROM likes l2 WHERE l2.post_id = p.id AND l2.user_id = ?) AS liked
        FROM posts p JOIN users u ON u.id = p.user_id
        WHERE (? IS NULL OR p.id < ?)
        ORDER BY p.id DESC
        LIMIT ' . ($limit + 1);
$stmt = db()->prepare($sql);
$before = $before ?: null;
$stmt->execute([$user['id'], $before, $before]);
$rows = $stmt->fetchAll();

$hasMore = count($rows) > $limit;
$rows    = array_slice($rows, 0, $limit);
$posts   = [];
foreach ($rows as $r) {
    $posts[] = [
        'id'        => (int) $r['id'],
        'type'      => $r['post_type'],
        'username'  => $r['username'],
        'caption'   => $r['caption'],
        'body_text' => $r['body_text'],
        'bg_color'  => $r['bg_color'],
        'mime_type' => $r['mime_type'],
        'video_url' => $r['post_type'] === 'video' ? presigned_url($r['s3_key']) : null,
        'likes'     => (int) $r['likes'],
        'liked'     => (bool) $r['liked'],
        'created_at'=> $r['created_at'],
    ];
}
$next = $hasMore && $posts ? end($posts)['id'] : null;
json_out(['posts' => $posts, 'next_before' => $next]);

Sample response (shortened):

{
  "posts": [
    { "id": 8, "type": "video", "username": "shahrukh", "caption": "Sunset at Nashik",
      "video_url": "https://ravindra-reels-media-pune.s3.ap-south-1.amazonaws.com/videos/2026/09/...&X-Amz-Expires=1200&X-Amz-Signature=...",
      "likes": 3, "liked": false },
    { "id": 7, "type": "text", "username": "zoya", "body_text": "Chala mitrano!", "bg_color": "#6A1B9A",
      "video_url": null, "likes": 1, "liked": true }
  ],
  "next_before": 4
}

like.php toggles a like. It accepts only POST with the CSRF token in an X-CSRF-Token header, and the composite primary key guarantees one like per user per post.

public/like.php

<?php
// public/like.php – toggle a like (POST + CSRF header), returns the new count
require __DIR__ . '/../src/bootstrap.php';

$user = require_login(true);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    json_out(['error' => 'POST only'], 405);
}
csrf_check();
$postId = filter_input(INPUT_POST, 'post_id', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if (!$postId) {
    json_out(['error' => 'bad post id'], 400);
}

$pdo = db();
$del = $pdo->prepare('DELETE FROM likes WHERE user_id = ? AND post_id = ?');
$del->execute([$user['id'], $postId]);
$liked = false;
if ($del->rowCount() === 0) {
    try {
        $ins = $pdo->prepare('INSERT INTO likes (user_id, post_id) VALUES (?, ?)');
        $ins->execute([$user['id'], $postId]);
        $liked = true;
    } catch (PDOException $ex) {
        json_out(['error' => 'post not found'], 404);        // foreign key failed
    }
}
$cnt = $pdo->prepare('SELECT COUNT(*) FROM likes WHERE post_id = ?');
$cnt->execute([$postId]);
json_out(['liked' => $liked, 'likes' => (int) $cnt->fetchColumn()]);

Why this matters for security

The feed returns only fields the UI needs – never password_hash, internal paths or raw S3 keys that are not presigned. feed.php requires login and returns 401 JSON instead of data. Notice before is validated with filter_input(... FILTER_VALIDATE_INT) and the LIMIT is a constant – user input never becomes part of the SQL text.

Ravindra Bagale's Tip

SELECT * karun sagla row JSON madhe pathavne ha students cha shortcut aahe – aani tyat password_hash pan jaato! API madhe nehmi fields ek ek nivdun pathva. Browser DevTools → Network madhe feed.php cha response ughda aani "he sagle field user la disle tari chalel ka?" asa prashna vichara.

Practice task

Open https://<your-server>/feed.php while logged in and read the JSON. Copy one video_url, open it in a private window (works), wait 21 minutes and open it again (expired). Log out and open feed.php again – you should get 401.