| title |
|---|
| Technical Design Document |
1. System overview
This document outlines the architecture and implementation blueprint for a full-stack Reddit clone called Discourse built by a team of three developers. The current implementation uses a component-based architecture with a Vue frontend, an Node.js API, and separate data services:
- A Vue 3 frontend bundled with Vite.
- A Node.js and Express backend providing the REST API.
- PostgreSQL for relational post data.
- Garage for S3-compatible object storage and uploaded images.
The frontend and backend are maintained as separate Git submodules inside a root infrastructure repository. The root repository owns the local integration environment, Docker Compose configuration, Garage configuration, and shared environment values. Repository setup and day-to-day development commands belong in the root (ref. Development workflow documentation).
The current system is a modular monolith rather than a microservice architecture. The backend owns API routing, validation, database access, health checks, upload URL generation, and storage setup. The frontend communicates with the backend over HTTP and uploads image content directly to object storage using a presigned URL.
2. System architecture
+----------------------+ REST API +------------------------+ SQL +-------------------+
| Vue 3 Client | -----------------> | Node.js / Express API | --------------> | PostgreSQL 18 |
| Vite development | | Docker container | | Relational data |
+----------+-----------+ +-----------+------------+ +-------------------+
| ^ |
| | fetch images via Garage web endpoint | S3 control API
| | (Direct GET) | sign/configure/check
| | v
| | +------------------------+
| +----------------------------+ | Garage S3 storage |
| | Images and web hosting |
+---------------------------------------> +------------------------+
direct PUT via presigned URL
Service topology
| Service | Container role | Local host endpoint | Internal service name |
|---|---|---|---|
| Frontend | Vue/Vite development server | http://localhost:5173 |
frontend:5173 |
| Backend | Express REST API and Swagger UI | http://localhost:5000 |
backend:5000 |
| PostgreSQL | Relational database | localhost:5432 |
db:5432 |
| Garage S3 API | S3-compatible object API | localhost:3900 |
s3:3900 |
| Garage web endpoint | Public object hosting | localhost:3902 |
s3:3902 |
| Garage admin API | Garage administration | localhost:3903 |
s3:3903 |
| Garage UI | Storage administration UI | http://localhost:8080 |
garage-ui:8080 |
Swagger UI and the OpenAPI JSON specification are served by the backend at /api-docs and /api-docs.json.
3. Component design & Tech stack
- Frontend: Vue 3 bundled with Vite, with Vue Router for the post feed and post creation views. It communicates with the backend API and uploads images directly to Garage through presigned URLs.
- Backend: Node.js runtime using Express.js for API routing, validation, post operations, health checks, image upload URL generation, and storage integration. Swagger UI and the generated OpenAPI specification are served by the backend.
- Database: PostgreSQL stores posts, optional image keys, and aggregate like and dislike counters. Database schema changes are applied through
node-pg-migrate. - Object storage: Garage provides S3-compatible storage for user-uploaded images. The backend manages storage configuration and presigned URLs, while the frontend sends image bytes directly to Garage.
4. API Specification Overview
The API is served by the backend on port 5000 in local development.
| Method | Endpoint | Description | Success | Main errors |
|---|---|---|---|---|
| GET | /api/health |
Check backend, PostgreSQL, and S3 availability. | 200 when all dependencies are available. |
503 when a dependency is unavailable. |
| GET | /api/posts |
Return posts ordered by newest ID first. | 200 with a JSON array. |
500 for an unexpected server error. |
| POST | /api/posts |
Create a post with a title, content, optional author, and optional image key. | 201 with the created post. |
400 for invalid title, content, or image key; 500 for server errors. |
| POST | /api/posts/:id/like |
Increment the like counter for a post. | 200 with the updated post. |
400 for an invalid ID; 404 when the post does not exist; 500 for server errors. |
| POST | /api/posts/:id/dislike |
Increment the dislike counter for a post. | 200 with the updated post. |
400 for an invalid ID; 404 when the post does not exist; 500 for server errors. |
| POST | /api/uploads |
Generate a presigned Garage upload URL and a UUID object key. | 200 with uploadUrl and fileKey. |
500 for storage errors. |
| GET | /api-docs |
Swagger UI endpoint. | 200 with the documentation interface. |
- |
| GET | /api-docs.json |
OpenAPI specification endpoint. | 200 with an OpenAPI document. |
- |
Post response shape example
{
"id": 1,
"author": "Anonymous",
"title": "Example title",
"content": "Example content",
"imageUrl": "http://server.example:3902/discourse/object-key",
"likeCount": 0,
"dislikeCount": 0
}
Interactive API documentation
The Swagger UI at /api-docs lets developers browse endpoints, view request and response examples, and try requests interactively. The generated OpenAPI specification is available at /api-docs.json.
5. Database schema
Table posts
| Column | Type | Constraints | Description |
|---|---|---|---|
id |
SERIAL |
PRIMARY KEY |
- |
author |
TEXT |
DEFAULT 'Anonymous' |
Name of the author for the post |
title |
VARCHAR(100) |
NOT NULL |
Title of the post |
content |
TEXT |
NOT NULL |
Main body of the post |
imageKey |
TEXT |
Nullable | Image key in S3 bucket |
like_count |
INTEGER |
NOT NULL, DEFAULT 0 |
Total number of likes received |
dislike_count |
INTEGER |
NOT NULL, DEFAULT 0 |
Total number of dislikes received |
6. Infrastructure
Versions of software used for the project:
- Node 26
- Express 5.2.1
- Postgres 18
- Garage
- node-postgres 8.23.0
- node-pg-migrate 9.0.0
- cors 2.8.6
- dotenv 17.4.2
- vue 3.5.41
- vue-router 5.3.1
Notable functions:
function runMigrations() in db.js
Runs migrations on the database.
async function loadPosts() in HomePage.vue
Loads posts onto the main page asyncronously