Introduction
The term craigslist clone PHP refers to web applications developed using the PHP programming language that replicate the functionality and design principles of the Craigslist online classifieds platform. These clones provide users with the ability to post, search, and manage classified advertisements across a range of categories such as housing, jobs, services, for sale, and community. By leveraging PHP’s extensive ecosystem, developers can create scalable, modular, and cost-effective solutions for local markets, niche communities, and specialized industries.
While Craigslist itself remains proprietary and heavily guarded, the open-source community has produced a variety of projects that offer similar features with different licensing models. These projects typically adopt the Model-View-Controller architectural pattern, utilize relational databases for data persistence, and expose a set of RESTful endpoints for third‑party integrations. The resulting applications are adaptable to multiple use cases, including regional classifieds, university bulletin boards, and business listing directories.
The article below presents a comprehensive overview of the key components, development practices, and operational considerations associated with building a Craigslist clone in PHP. It also explores the historical context of online classifieds, outlines security and performance best practices, and discusses emerging trends that influence the evolution of such platforms.
History and Background
Online classified advertising emerged in the late 1990s as one of the earliest forms of web‑based e‑commerce. The first commercial classifieds service, eBay Classifieds, launched in 1998, providing a digital space for individuals to post items for sale or request services. The advent of Craigslist in 1995 marked a significant shift toward a minimalist, community‑centric model that prioritized simplicity and local relevance.
Craigslist’s growth was fueled by its free posting policy, regional focus, and a highly streamlined user interface. The platform’s success demonstrated the viability of low‑cost, high‑volume web services that required minimal infrastructure. As a result, many developers began experimenting with PHP, a language that had matured to support dynamic web development and became the de‑facto choice for rapid prototyping of classified sites.
In the early 2000s, several open‑source PHP frameworks such as CodeIgniter, Laravel, and Symfony gained popularity. These frameworks offered built‑in features like routing, ORM, and templating that accelerated the creation of Craigslist‑style applications. Additionally, community‑generated repositories on platforms like GitHub introduced reusable modules for search, geolocation, and user management, further lowering the barrier to entry.
Today, the market for classified platforms remains robust, with thousands of PHP‑based clones available. Many of these projects are maintained under permissive licenses such as MIT or Apache, encouraging commercial and non‑commercial deployment. Despite the presence of large, well‑funded competitors, the open‑source model continues to attract developers who value flexibility, transparency, and the ability to tailor features to specific audiences.
Key Concepts
Architecture
Craigslist clones typically employ a layered architecture that separates concerns across data access, business logic, and presentation layers. The most common pattern is the Model‑View‑Controller (MVC) model:
- Model – Handles database interactions and encapsulates business rules.
- View – Presents data to the user through HTML templates, often augmented by JavaScript for dynamic behavior.
- Controller – Receives HTTP requests, orchestrates data retrieval, and renders appropriate views.
Additional components may include a service layer for complex operations such as geospatial queries, a notification subsystem for email or SMS alerts, and a caching layer that improves read performance.
Data Model
The underlying data model of a Craigslist clone centers around four primary entities: Users, Posts, Categories, and Comments. These entities are represented as tables in a relational database (commonly MySQL or MariaDB). Typical attributes include:
- User – id, username, email, hashed password, role, status, timestamp.
- Post – id, userid, title, description, price, imagepaths, categoryid, city, state, zip, latitude, longitude, createdat, updated_at, status.
- Category – id, parent_id, name, slug, description.
- Comment – id, postid, userid, content, createdat, isapproved.
Normalization ensures data consistency, while indexes on frequently searched columns such as city and price accelerate query execution. Many projects also incorporate a tags table to enable keyword‑based filtering.
Search and Filtering
Efficient search functionality is central to the user experience. Basic implementations rely on SQL LIKE clauses and full‑text indexes to match keywords within titles and descriptions. Advanced systems may integrate specialized search engines like Elasticsearch or Apache Solr to provide faceted navigation, relevance scoring, and autocomplete features.
Geolocation filtering is another core requirement. Posts are often tagged with latitude and longitude coordinates. Geospatial indexing (e.g., using MySQL's SPATIAL indexes) allows for radius queries, enabling users to find listings within a specified distance from a given point.
User Interaction
Typical user flows include:
- Registration and account verification.
- Posting a new advertisement with optional images.
- Editing or deleting existing posts.
- Searching and filtering listings.
- Commenting on or inquiring about posts.
- Managing notifications and account settings.
Many clones implement a responsive design that adapts to mobile browsers. JavaScript frameworks like jQuery or Vue.js are often used to provide instant feedback and form validation.
Payment Integration
While Craigslist originally did not support paid listings, modern clones frequently expose payment gateways (PayPal, Stripe, Braintree) to facilitate paid advertising or premium features such as featured listings and email alerts. Payments are typically processed through server‑side API calls that generate transaction records linked to the corresponding post.
Moderation and Spam Prevention
Automated moderation pipelines evaluate new posts against predefined rules. Common checks include:
- Keyword blacklists to filter inappropriate content.
- Image analysis to detect disallowed media.
- Rate limiting to prevent bulk posting.
- CAPTCHA challenges to mitigate bot submissions.
Administrators often access a dedicated dashboard to review flagged content, approve or reject posts, and manage user privileges.
Technical Implementation
Framework Selection
Choosing a PHP framework influences the development lifecycle. Popular options include:
- Laravel – Offers expressive routing, Eloquent ORM, built‑in authentication scaffolding, and a robust ecosystem of packages.
- Symfony – Emphasizes reusable components, dependency injection, and a modular architecture suitable for large‑scale projects.
- CodeIgniter – Lightweight with minimal overhead, ideal for small to medium‑sized applications.
Framework choice depends on project requirements, team expertise, and hosting constraints.
Database Design
Normalization and indexing are crucial for performance. Key practices include:
- Use surrogate keys (auto‑increment integers) for primary identifiers.
- Create composite indexes on columns frequently used together in queries, such as
(city, price). - Leverage full‑text indexes on
titleanddescriptionto accelerate keyword searches. - Implement foreign key constraints to maintain referential integrity.
- Employ database partitioning or sharding for very large datasets.
Routing and Controllers
Controllers translate HTTP requests into business operations. Typical routes include:
/posts– GET to list posts, POST to create a new post./posts/{id}– GET to view a specific post, PUT/PATCH to update, DELETE to remove./search– GET with query parameters for filters./login,/register,/logout– Authentication endpoints.
RESTful conventions encourage clear mapping between HTTP verbs and CRUD actions, improving maintainability.
Authentication and Authorization
Secure authentication mechanisms typically involve:
- Hashing passwords with algorithms such as bcrypt or Argon2.
- Using session tokens or JSON Web Tokens (JWT) for stateless authentication.
- Implementing role‑based access control to differentiate between regular users and administrators.
- Providing email verification or two‑factor authentication for enhanced security.
File Upload Handling
Posts often include images or documents. Upload logic must address:
- Validating MIME types and file extensions to prevent malicious uploads.
- Resizing and compressing images using libraries like GD or Imagick.
- Storing files in a dedicated directory structure or cloud storage services (e.g., Amazon S3).
- Generating unique filenames to avoid collisions.
Front‑End Rendering
Template engines such as Blade (Laravel), Twig (Symfony), or plain PHP are used to separate presentation from logic. Common front‑end features include:
- Responsive grid layouts for listing displays.
- Interactive maps using APIs like Google Maps or OpenStreetMap.
- Modal dialogs for posting forms and login screens.
- Real‑time notifications via WebSockets or long polling.
Testing and Quality Assurance
Automated testing frameworks (PHPUnit, Behat) enable unit, integration, and behavior‑driven tests. Continuous integration pipelines run tests on each commit, ensuring regression prevention and code quality. Code style tools (PHP_CodeSniffer, PHP-CS-Fixer) enforce consistent formatting, while static analysis tools (Psalm, PHPStan) detect potential runtime errors.
Security Considerations
Input Validation
Sanitizing all user input prevents injection attacks. PHP’s filter functions, combined with parameterized SQL queries, mitigate SQL injection. Output encoding (e.g., htmlspecialchars) protects against cross‑site scripting (XSS).
Cross‑Site Request Forgery (CSRF)
Tokens embedded in forms and verified on the server side prevent unauthorized state changes. Frameworks often provide CSRF middleware that automates this process.
Authentication Security
Strong password policies, account lockout after multiple failed attempts, and secure cookie flags (HttpOnly, Secure, SameSite) enhance authentication security. Password resets are time‑bound and delivered via email with unique tokens.
File Upload Security
Beyond MIME type checks, applications should verify file contents and enforce size limits. Storing uploaded files outside the webroot or serving them through controlled scripts adds an additional layer of protection.
Rate Limiting and DoS Protection
Implementing per‑IP or per‑user rate limits on critical endpoints (login, posting) reduces the risk of abuse. Application firewalls and traffic monitoring tools can detect anomalous patterns and trigger mitigation responses.
Data Encryption
Transport Layer Security (TLS) encrypts all network traffic. Sensitive data at rest, such as payment details, should be encrypted using industry standards, complying with regulations like PCI‑DSS or GDPR.
Performance and Scalability
Caching Strategies
Layered caching improves response times:
- Database query caching using query result caches or materialized views.
- Application caching with Redis or Memcached for session storage and frequently accessed data.
- HTTP caching headers and CDN edge caching for static assets.
Database Optimization
Index maintenance, query profiling, and partitioning help manage large volumes of listings. Denormalization may be employed for read‑heavy scenarios, such as storing a cached copy of aggregated statistics.
Horizontal Scaling
Deploying multiple application servers behind a load balancer distributes traffic. Stateless session handling or shared session stores ensure consistency across instances.
Asynchronous Processing
Background job queues (Laravel Queues, Symfony Messenger) handle resource‑intensive tasks such as image processing, email dispatch, and notification delivery. This decouples user actions from long‑running operations, preserving a responsive interface.
Geospatial Performance
Spatial indexes accelerate proximity searches. For very large datasets, spatial databases like PostGIS or external services such as GeoServer can offload complex queries.
Deployment and Hosting
Traditional LAMP Stack
Many PHP applications are hosted on Linux servers running Apache or Nginx, PHP-FPM, and MySQL. This configuration offers stability and compatibility with most control panels.
Containerization with Docker
Docker containers encapsulate application dependencies, enabling consistent deployment across environments. Compose files define multi‑service architectures, including web, database, cache, and worker processes.
Cloud Platforms
Public cloud providers (Amazon Web Services, Google Cloud Platform, Microsoft Azure) offer managed services such as RDS for databases, Elasticache for caching, and S3 for object storage. Serverless architectures, using functions (e.g., AWS Lambda), can host stateless components like API endpoints.
Continuous Integration / Continuous Deployment (CI/CD)
Automated pipelines trigger tests, build artifacts, and deployment scripts upon code commits. Tools such as GitHub Actions, GitLab CI, or Jenkins facilitate reproducible releases and rollback capabilities.
Community and Ecosystem
The development of Craigslist clones is supported by a vibrant community of open‑source contributors. Key aspects include:
- Repositories – Projects such as php-classified, Open Classifieds, and Yii2 Classified provide modular codebases.
- Forums – Discussion boards on platforms like Stack Overflow and Reddit allow users to share solutions, troubleshoot issues, and propose enhancements.
- Packages – Composer packages for image manipulation, geolocation, and authentication reduce integration effort.
- Events – Meetups, conferences, and hackathons often showcase use cases and best practices.
Documentation and tutorials are frequently updated, ensuring newcomers can onboard quickly.
Case Studies
Local Marketplace Implementation
In a regional startup, a Laravel‑based classified platform was launched with featured listings and local delivery integration. By employing Redis caching and CDN edge caching, the platform achieved sub‑300‑millisecond page loads during peak traffic.
Community‑Based Portal
A city council deployed a Symfony‑based classified portal to allow residents to post community events, job listings, and neighborhood services. The portal integrated Stripe for paid sponsorships and used OpenStreetMap for free mapping. The modular Symfony components facilitated rapid addition of new modules (forum, forum moderation).
Future Directions
Emerging trends shaping the next generation of classified platforms include:
- Artificial Intelligence (AI)‑driven content recommendation and personalization.
- Blockchain‑based reputation systems for user trust.
- Advanced analytics dashboards leveraging machine learning to detect market trends.
- Voice‑enabled interfaces and conversational agents for hands‑free interaction.
As technology evolves, developers will continue to adapt the core concepts of classified platforms, balancing user convenience, business models, and security.
Conclusion
Building a classified advertisement platform akin to Craigslist demands a comprehensive understanding of web architecture, user experience, and security best practices. By following structured design principles - clean database schemas, robust routing, secure authentication, and layered performance optimizations - developers can create scalable, maintainable, and secure applications. The thriving open‑source community, coupled with modern deployment strategies, empowers teams to iterate quickly and deliver features that meet the evolving needs of online classifieds.
); $pdo->exec("INSERT INTO posts (title, content, user_id) VALUES ('First Post', 'This is the first post content', 1)"); - This code snippet demonstrates the insertion of a post into a database within a PHP context, showcasing how to handle database operations securely and efficiently.
References
- https://www.oreilly.com/ - https://www.raspberrypi.org - https://www.github.com - https://www.stackoverflow.com- https://www.freshworks.com`,
createdAt: new Date(),
},
];
}import type { User } from '@/models/User';
import type { Comment } from '@/models/Comment';
export interface Thread {
id: string;
title: string;
type: 'thread';
typeName: string;
createdAt: Date;
lastReply: Date;
author: User;
tags: string[];
comments?: Comment[];
commentCount: number;
viewCount: number;
likes: number;
likesCount: number;
dislikes: number;
dislikesCount: number;
replies: Thread[];
}
| Name | Role | Created At | |
|---|---|---|---|
| {{ user.name }} | {{ user.email }} | {{ user.role?.name }} | {{ formatDate(user.createdAt) }} |
id: string;
title: string;
description: string;
category?: string;
status?: string;
createdAt?: Date;
updatedAt?: Date;
isFeatured?: boolean;
isDeleted?: boolean;
isApproved?: boolean;
isHidden?: boolean;
author?: string;
authorName?: string;
authorEmail?: string;
authorImage?: string;
authorImageUrl?: string;
authorImageAlt?: string;
imageUrls?: string[];
imageUrlsCount?: number;
viewCount?: number;
likes?: number;
likesCount?: number;
dislikes?: number;
dislikesCount?: number;
comments?: Comment[];
commentCount?: number;
}
import { ref, computed, onMounted, watch } from 'vue';
import type { User } from '@/models/User';
import type { Api } from '@/providers/Api';
import { useI18n } from 'vue-i18n';
export function useAdminUsers(api: Api) {
const { t } = useI18n();
const users = reftry {
const response = await api.get('/api/admin/users', {
params: {
page: page.value,
perPage: perPage.value,
},
});
users.value = response.data.data;
totalUsers.value = response.data.total;
maxPages.value = response.data.totalPages;
} catch (error) {
console.error('Error fetching users:', error);
}
};
watch([page, perPage], fetchUsers);
onMounted(fetchUsers);
const deleteUser = async (userId: string) => {
if (!confirm(t('confirmDelete'))) return;
try {
await api.delete(/api/admin/users/${userId});
users.value = users.value.filter((user) => user.id !== userId);
console.log(t('userDeleted'));
} catch (error) {
console.error('Error deleting user:', error);
}
};
const setRole = async (userId: string, roleId: string) => {
try {
await api.patch(/api/admin/users/${userId}/role, { roleId });
console.log(t('roleUpdated'));
} catch (error) {
console.error('Error setting role:', error);
}
};
const searchUsers = async (query: string) => {
try {
const response = await api.get('/api/admin/users/search', {
params: { query },
});
users.value = response.data;
} catch (error) {
console.error('Error searching users:', error);
}
};
return {
users,
page,
perPage,
maxPages,
totalUsers,
fetchUsers,
deleteUser,
setRole,
searchUsers,
};
}export const useImageUpload = (api) => {
const uploadImage = async (imageFile) => {
const formData = new FormData();
formData.append('image', imageFile);
try {
const response = await api.post('/api/images', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
if (response.status === 200) {
return response.data.url;
} else {
throw new Error('Image upload failed');
}
} catch (error) {
console.error('Error uploading image:', error);
throw error;
}
};
return {
uploadImage,
};
};export const formatDate = (date, format = 'short') => {
// ... existing implementation ...
return formattedDate; // This will be a string
};
Total posts
{{ posts?.length }}
Total comments
{{ comments?.length }}
Total users
{{ users?.length }}
{{ post.title }}
{{ post.title }}Comments ({{ post.commentCount }})
{{ comment.message }}
{{ comment.user.name }}
Replies ({{ post.repliesCount }})
{{ reply.content }}
{{ reply.user.name }}
Comments
{{ post.title }}
{{ post.viewCount }}
Comments ({{ post.commentCount }})
{{ comment.message }}
Replies ({{ post.repliesCount }})
{{ reply.content }}
Comments
Posted on: {{ formatDate(post.createdAt) }}
Comments ({{ post.comments.length }})
Comments ({{ post.comments.length }}){{ comment.authorName }}
{{ comment.message }}
{{ comment.createdAt }}
Replies ({{ post.replies.length }})
Replies ({{ post.replies.length }}){{ reply.authorName }}
{{ reply.message }}
{{ reply.createdAt }}
{{ post.title }}
Posted on {{ post.created_at }}
{{ post.view_count }} views
{{ post.comment_count }} comments
1. Project structure
/src ├─ app.js # Express entry point ├─ routes/ │ └─ posts.js # Routes for posts, comments & replies ├─ controllers/ │ └─ postCtrl.js # All the business logic ├─ models/ │ ├─ Post.js │ ├─ Comment.js │ └─ Reply.js └─ middleware/└─ auth.js # (optional) JWT / session middleware
---
2. Mongoose Schemas
js // src/models/Post.js const mongoose = require('mongoose'); const Schema = mongoose.Schema; const postSchema = new Schema({ title: { type: String, required: true }, content: { type: String, required: true }, createdAt: { type: Date, default: Date.now }, viewCount: { type: Number, default: 0 }, likeCount: { type: Number, default: 0 }, likedBy: [{ type: Schema.Types.ObjectId, ref: 'User' }], // optional }); module.exports = mongoose.model('Post', postSchema); js // src/models/Comment.js const commentSchema = new Schema({ post: { type: Schema.Types.ObjectId, ref: 'Post', required: true }, author: { type: Schema.Types.ObjectId, ref: 'User', required: true }, authorName:{ type: String, required: true }, message: { type: String, required: true }, createdAt: { type: Date, default: Date.now }, }); module.exports = mongoose.model('Comment', commentSchema); js // src/models/Reply.js const replySchema = new Schema({ post: { type: Schema.Types.ObjectId, ref: 'Post', required: true }, author: { type: Schema.Types.ObjectId, ref: 'User', required: true }, authorName:{ type: String, required: true }, message: { type: String, required: true }, createdAt: { type: Date, default: Date.now }, }); module.exports = mongoose.model('Reply', replySchema); > **Tip** – If you don’t need separate `User` refs you can omit them; the example assumes you have a `User` model elsewhere. ---3. Controller (src/controllers/postCtrl.js)
js
const Post = require('../models/Post');
const Comment = require('../models/Comment');
const Reply = require('../models/Reply');
// --------------------------------------------------------
// GET /posts/:id – fetch a post by id (also bumps viewCount)
exports.getPostById = async (req, res) => {
try {
const post = await Post.findById(req.params.id)
.populate('likedBy', 'username')
.exec();
if (!post) return res.status(404).json({ error: 'Post not found' });
// Increment view count
post.viewCount += 1;
await post.save();
// Get comments & replies
const comments = await Comment.find({ post: post._id })
.sort({ createdAt: -1 });
const replies = await Reply.find({ post: post._id })
.sort({ createdAt: -1 });
res.json({
...post.toObject(),
comments,
replies,
});
} catch (err) {
res.status(500).json({ error: err.message });
}
};
// --------------------------------------------------------
// POST /posts/:id/like – toggle like by current user
exports.toggleLike = async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) return res.status(404).json({ error: 'Post not found' });
const userId = req.user._id; // assumes req.user set by auth middleware
const liked = post.likedBy.includes(userId);
if (liked) {
post.likedBy.pull(userId);
post.likeCount -= 1;
} else {
post.likedBy.push(userId);
post.likeCount += 1;
}
await post.save();
res.json({ liked: !liked, likeCount: post.likeCount });
} catch (err) {
res.status(500).json({ error: err.message });
}
};
// --------------------------------------------------------
// PUT /posts/:id – edit post content
exports.updatePost = async (req, res) => {
try {
const { title, content } = req.body;
const post = await Post.findById(req.params.id);
if (!post) return res.status(404).json({ error: 'Post not found' });
// (Optional) restrict edit to author only
if (!post.author.equals(req.user._id))
return res.status(403).json({ error: 'Not authorized' });
if (title !== undefined) post.title = title;
if (content !== undefined) post.content = content;
await post.save();
res.json(post);
} catch (err) {
res.status(500).json({ error: err.message });
}
};
// --------------------------------------------------------
// DELETE /posts/:id – delete the post (after confirmation)
exports.deletePost = async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) return res.status(404).json({ error: 'Post not found' });
// (Optional) restrict delete to author only
if (!post.author.equals(req.user._id))
return res.status(403).json({ error: 'Not authorized' });
// Remove all comments/replies first
await Comment.deleteMany({ post: post._id });
await Reply.deleteMany({ post: post._id });
await post.remove();
res.json({ success: true, message: 'Post deleted' });
} catch (err) {
res.status(500).json({ error: err.message });
}
};
> **What each endpoint does**
> * `getPostById` – fetches the post, auto‑increments view count, and pulls in comments/replies.
> * `toggleLike` – toggles the like state for the logged‑in user and returns the new count.
> * `updatePost` – edits title/content, only if the requester owns the post.
> * `deletePost` – removes the post, its comments & replies (you can add any confirmation logic on the front‑end).
---
4. Routes (src/routes/posts.js)
js
const express = require('express');
const router = express.Router();
const postCtrl = require('../controllers/postCtrl');
const auth = require('../middleware/auth'); // optional
// 1️⃣ GET a post by id (incl. views, comments & replies)
router.get('/:id', auth.optional, postCtrl.getPostById);
// 2️⃣ Like / unlike a post
router.post('/:id/like', auth.required, postCtrl.toggleLike);
// 3️⃣ Edit a post
router.put('/:id', auth.required, postCtrl.updatePost);
// 4️⃣ Delete a post
router.delete('/:id', auth.required, postCtrl.deletePost);
module.exports = router;
> **Authentication** – The `auth.required` middleware should populate `req.user` with the current logged‑in user (JWT, session, etc.).
> If you’re not using auth, remove the `auth` middleware and simply hard‑code a `userId` for testing.
---
5. Express bootstrap (src/app.js)
js
const express = require('express');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const postsRoutes = require('./routes/posts');
const app = express();
const PORT = process.env.PORT || 3000;
// --- Middleware ----------------------------------------------------
app.use(bodyParser.json());
// (optional) connect your own auth middleware here
// const auth = require('./middleware/auth');
// app.use(auth);
// --- Routes ---------------------------------------------------------
app.use('/posts', postsRoutes);
// --- Connect to MongoDB --------------------------------------------
mongoose.connect(process.env.MONGO_URI || 'mongodb://localhost:27017/post_demo',
{ useNewUrlParser: true, useUnifiedTopology: true })
.then(() => {
console.log('✅ Connected to MongoDB');
app.listen(PORT, () => console.log(`🚀 Server listening on port ${PORT}`));
})
.catch(err => console.error('❌ MongoDB connection error:', err));
---
6. Quick test via cURL / Postman
| Method | URL | Description | |--------|-----|-------------| | `GET` | `/posts/:id` | Returns a single post with comments & replies, auto‑increments view counter | | `POST` | `/posts/:id/like` | Toggles like for the logged‑in user (requires auth) | | `PUT` | `/posts/:id` | Update `title` / `content` (JSON body) – only if you’re the author | | `DELETE` | `/posts/:id` | Deletes the post and its children (confirmation is front‑end) | Example: Edit a post bash curl -X PUT http://localhost:3000/posts/5f5c8b4e4a1b2c0012345678 \-H "Content-Type: application/json" \
-d '{"title":"New title","content":"Updated body"}'
Example: Like a post
bash
curl -X POST http://localhost:3000/posts/5f5c8b4e4a1b2c0012345678/like
---
7. Real‑time updates (optional)
If you want the like count to update instantly on **all connected clients**, swap the `toggleLike` route for a **WebSocket** (e.g. Socket.io) event that emits the new count to all listeners. Otherwise, the simple `PUT`/`POST` response is fine for a polling / “optimistic UI” approach. ---8. Summary
- GET /posts/:id – fetches the post, auto‑increments view count, and returns all comments & replies.
- POST /posts/:id/like – toggles a like for the current user and sends back the updated count.
- PUT /posts/:id – edits the post (title &/or content).
- DELETE /posts/:id – removes the post and its child documents after front‑end confirmation.
No comments yet. Be the first to comment!