Skip to content

Repository files navigation

Smiler

Smiler is a Reddit-style social platform where users share posts with text, images, and videos, follow creators and topics, and engage through comments and ratings.

Table of Contents

Key Features

This project is built on the MEVN stack (MongoDB, Express, Vue.js, Node.js), written end-to-end in TypeScript, and is a Single Page Application (SPA) inspired by platforms like Reddit and 9gag. Below are the key features that make this project stand out:

Common Features

  • Containerized with Docker: The project is fully containerized using Docker, and the docker-compose setup is highly flexible. It can also run without Docker if needed.
  • pnpm Monorepo: Backend and frontend live in one workspace with shared tooling — Oxlint (plus ESLint for Vue templates), Prettier, cspell, and type-checking all run from the root.
  • TypeScript End to End: Both packages are fully typed, including the API client, which shares request and response types with the components that use them.

Backend Key Features

  • Core Infrastructure:

    • Clustering: If an exception occurs on the server, the app won't crash. Instead, another instance will be spawned to keep the application running.
    • CORS Protection: The API restricts access to allowed domains via environment variables.
    • Rate Limiting: Read and write endpoints are protected by separate rate limiters, counted in Redis so every cluster worker and every host shares one budget per client.
    • Sessions in Redis: Sessions live in Redis rather than the main database, which keeps the hottest read of an authenticated request out of MongoDB.
    • CSRF Protection: State-changing requests require a CSRF token, issued and validated per session.
    • Input Sanitizing and Validation: Rich text from posts and comments is sanitized server-side, and every endpoint validates its payload before it reaches the database.
    • Logging: Every request is logged using Winston and morgan as structured JSON — one object per line, carrying a request id, the matched route template and the acting user, so a report can be traced from the client's X-Request-Id to the error that caused it. Logs go to stdout and are collected and rotated by Docker.
    • Integration Testing: The backend is rigorously tested with integration tests to ensure real-world functionality. These tests verify that ORM functions interact correctly with the database, API endpoints return the expected data, a file is actually written to disk or deleted, and dependencies (e.g., ORM, middleware) work as intended, even after updates and so on.
  • Posts:

    • Creating Posts: Users can create posts with a slug generated from the title. Posts are composed of "sections," which can be text, pictures, or video links. Sections keep their order in the database, and users can add up to 8 sections per post.
    • Uploading Pictures: Images can be uploaded directly or given as a link, which the server downloads. Either way the picture is re-encoded, resized and stored on this server, so posts never hotlink a third party and every picture is bounded in size.
    • Updating and Deleting Posts: Users can update or delete their posts, but only within 10 minutes of creation.
    • Feed: View the latest posts from followed users or tags.
    • Tags: Posts can have up to 8 tags, and users can follow or unfollow tags to customize their feed.
    • Post Retrieval: Posts can be retrieved with pagination and filtered by author, date, rating, or title regex. The API also shows if the user has already rated a post.
    • Post Rating: Posts have a rating system that contributes to the user's overall rating.
  • Users:

    • Profile Picture: Users can set a profile picture using a link.
    • Following Users: Users can follow or unfollow other users.
    • Bio: Users can add a short description about themselves.
    • Registration and Authentication: Standard registration and authentication features are implemented.
    • Sessions: The app uses sessions instead of JWT for better security.
    • Saving Drafts: Users can save post drafts, including sections, title, and body, without publishing them.
    • Individual Rating: Each user has a rating based on the sum of ratings for their posts and comments.
  • Comments:

    • Hierarchical Comment Tree: Comments are displayed in a nested tree structure, with recursive checks to show if the user has already rated a comment.
    • Creating, Updating, and Deleting Comments: Users can create, update, or delete comments within a specific time frame, provided no one has replied to them.
    • Comment Rating: Comments have a rating system that contributes to the user's overall rating.
  • Swagger Documentation: Full API documentation is available at /api-docs/, generated from the same schemas the server validates requests against, with the raw OpenAPI 3.1 document at /api-docs/openapi.json.

Frontend Key Features

  • Core Features:

    • Auth Guards: Routes requiring authentication are protected.
    • Allowed Routes Guard: Non-existent routes redirect to a 404 page.
    • Expired Actions Guard: Prevents access to actions like editing a post after the allowed time has passed.
    • Global Request Error Notifications: Errors trigger animated notifications that disappear after a few seconds.
    • Adaptive Design: The frontend is fully responsive.
    • Light and Dark Themes: A theme toggle backed by CSS custom properties, remembering the user's choice.
    • Dynamic Document Title: The page title updates dynamically based on the route.
    • End-to-End (E2E) Testing: Most frontend components are covered with E2E tests to ensure reliability and a smooth user experience. These tests simulate real user interactions and validate the functionality of the application.
    • Unit Testing: Stores and standalone components are additionally covered with Vitest unit tests.
  • Posts:

    • Multiple Post Pages: Includes Today, All, Blowing, Top This Week, and New, each with unique sorting and filtering.
    • Post Editor: A Tiptap-based rich text editor for creating and editing posts, with drag-and-drop reordering of sections.
    • Preloader: Smooth loading animations for better UX.
    • Infinite Scroll: Loads more posts as you scroll.
    • Search with Filters: Search posts with advanced filters.
    • Following Tags: Follow or unfollow tags directly from the UI.
    • Collapsible Content: Long posts are collapsed in feeds and can be expanded in place.
  • Users:

    • Auth State Management: Handles authentication state, hiding unavailable features for logged-out users.
    • User Profile Page: Displays user posts, rating, followers, bio, and avatar.
    • Follow/Unfollow Users: Follow or unfollow other users.
    • Settings: Manage your profile, including bio, avatar, and followed users/tags.
    • Registration and Login: Standard registration and login forms.
  • Comments:

    • Tree Comments: Nested comments with a hierarchical structure.
    • Rich Text Editor: For creating and updating comments.
    • Delete and Update Comments: Users can delete or update their comments within a specific time frame.

Motivation

I built Smiler to practice building a full-stack application from scratch, working with the full MEVN stack, experimenting with different architectural patterns and testing strategies along the way. It's a playground for trying out new ideas and seeing what works (and what doesn't) in a real project context — from clustering and file uploads to hierarchical comments and multi-section posts. Starting from February 2026, I also use this project to learn and get comfortable working with agentic coding tools (vibecoding).

Project Timeline

Smiler has been in development since 2019 and has been through several full-stack rewrites along the way. The collapsed timeline below highlights the milestones — stack migrations, major features, and large refactors.

Show the project timeline (2019 → today)

2019 — The beginning

  • Aug 2019 — Project starts as an Express + MongoDB/Mongoose REST API: posts, users with sessions-based auth, a recursive comment tree, file uploads with image resizing, and Node.js clustering so a crashed worker never takes the app down.
  • Sep 2019 — Swagger documentation added for the whole API. The Vue 2 frontend (vue-cli) is bootstrapped and the app gets its first deployment to Heroku. Auth UI, user profiles, and the post/comment rating system land.
  • Oct 2019 — Swagger 2.0 → OpenAPI 3.0.0. Posts are remodeled around "sections" (text, picture, picture link, video) instead of a plain body — the single biggest data-model change in the project's history. A custom rich-text post editor, tags, following users and tags, the personal feed, search with filters, infinite scroll, and the mobile layout all arrive in the same month.
  • Dec 2019 — Drag-and-drop reordering for post editor sections. Backend gets Winston request/response logging and global error handling.

2020–2022 — Dormant years

  • 2020 — Mostly dependency and security bumps. Session cookies reworked for secure / sameSite so the deployed app works over HTTPS.
  • Apr 2021 — Swagger JSDoc comments rewritten as a standalone JSON spec.
  • 2022 — Not a single commit. The project sat untouched for the whole year.

2023 — Revival and containerization

  • Feb 2023 — The project comes back to life: Docker + Docker Compose for frontend, backend, and MongoDB, a large dependency upgrade across both packages, and flexible layered configs for local vs. published images.
  • Apr 2023 — Frontend build tooling migrates from vue-cli to Vite.
  • Jul 2023 — Deployment moves off Heroku to its own domain.
  • Dec 2023 — First Playwright E2E setup with mocked API fixtures.

2024 — Tests, tooling, and hardening

  • Jan–May 2024 — A full E2E test suite is built out: posts, single post, editor, auth, profile, settings, search, tags, comments. Tests are then refactored twice — first onto an API Page Object, then onto page objects for every element — and factories move to faker.js.
  • Mar 2024 — Linting infrastructure: Stylelint for Vue/PostCSS, Prettier, a reconfigured ESLint, and a custom Stylelint rule enforcing two-dash BEM (later published as its own npm package). Backend MongoDB indexes are made explicit and optimized.
  • May 2024 — cspell spellchecking with a project dictionary. Backend controllers split into per-entity files.
  • Jun 2024 — Frontend design-system pass: SCSS variables → CSS custom properties, moment.js → date-fns, SVGO-optimized icons, Concentric CSS property ordering, and a component-folder restructure with import aliases. sanitize-html added for user content.
  • Jul 2024 — Security and observability: helmet.js, timing-attack and unsanitized-regex fixes, JSON-formatted logs with multiple transports, and post categories split from one endpoint into separate ones. Jest + mongodb-memory-server arrive — the first backend tests.
  • Aug 2024 — Large error-handling refactor: typed HTTP error classes, a single global handler, and async/await throughout the controllers, replacing ad-hoc try/catch and promise chains. All updating endpoints start returning the updated document so the client can sync from responses. A full UI redesign ships at the end of the month.
  • Sep 2024 — Pagination reworked with total / hasNextPage. Backend converts from CommonJS to ES modules (type: module). Sessions key off userId instead of userLogin.
  • Nov 2024 – Feb 2025 — A months-long campaign of backend integration tests covering nearly every endpoint: posts, comments, auth, users, tags, categories, votes, and uploads.
  • Dec 2024 — Repo moves from npm to a pnpm monorepo with a single shared Dockerfile. knip added to hunt dead code and unused dependencies.

2025 — TypeScript everywhere

  • Mar 2025 — Backend migrates from JavaScript to TypeScript: full .js → .ts rename, typed controllers, and Swagger built at runtime. Mongoose → Typegoose, with Mongoose itself jumping from v5 to v8.
  • Apr 2025 — Express 4 → Express 5. Typed request/response contracts for every controller, remapped TS path aliases across source and tests, and swc for faster compilation.
  • Apr–May 2025 — Vue 2.7 → Vue 3.5, the frontend's biggest migration: new global API, updated v-model/event syntax, transition classes, and a patched vuedraggable. Vuex 4 → Pinia follows days later.
  • May–Jul 2025 — Every component is rewritten to Composition API + TypeScript (<script setup lang="ts">), the API client is rebuilt with per-endpoint types, and vue-tsc starts type-checking the whole app.
  • Aug 2025 — Voting system rework: votes can be flipped to the opposite direction, and deleting a post or comment now correctly rolls back the author's rating and removes the rate documents.
  • Sep 2025 — Playwright tests, factories, page objects, and route objects migrated from JS to TypeScript with their own tsconfig.

2026 — Security, DX, and agentic coding

  • Feb 2026 — Custom directives replaced with VueUse. This is also when the project starts being used to learn agentic coding tools.
  • Mar 2026 — Light/dark theme system with a full color-variable refactor, plus Vitest unit tests alongside the E2E suite. ESLint, Prettier, and TypeScript versions aligned across packages.
  • Apr 2026 — security — The largest security hardening push so far: API and write rate limiting, a path-traversal fix in upload/delete, CSRF protection end to end, hardened sessions, uploads, request logging and request IDs, stricter post/comment/profile validation, unique email and login indexes with normalization, and server-side-only section hashes. User rates move into their own collection with a MongoDB migration script.
  • Apr 2026 — editor & DX — The hand-rolled text editor is replaced with Tiptap, with rich-text sanitizing on the backend. husky + lint-staged git hooks added, AGENTS.md written, and the backend switches to lean() everywhere (id → _id across both packages).
  • May–Aug 2026 — Collapsible post content in feeds, a reusable modal/confirm-dialog system with unit and E2E coverage, and steady dependency upkeep.
  • Aug 2026 — security & performance — Stronger password hashing with gradual migration of existing hashes, case-normalized email/login lookups, lazy-loaded route components for a much smaller bundle, and a reworked graceful shutdown. A follow-up hardening pass widens the SSRF denylist, ties uploaded pictures to their author, forces a hardened rel on sanitized links, requires a browser origin for every state-changing request, and adds a Content Security Policy to the nginx config. List endpoints drop their per-request countDocuments, and the lists sorted by date of creation gain cursor-based pagination, so infinite scroll no longer repeats posts when new ones land mid-scroll.
  • Aug 2026 — database — MongoDB upgraded 5.0 → 8.0 one major at a time, with the test suite switched to clearing collections instead of dropping the database between runs.
  • Aug 2026 — runtime — Node.js upgraded 20 → 24 LTS across the dev environment, the engines fields, and the Docker images, with @types/node moved to the matching major. Jest's config loader is pinned to ts-node so the new native type stripping in Node doesn't try to load jest.config.ts as ESM.
  • Aug 2026 — commit conventions — The repo adopts Conventional Commits (the Angular flavour), validated and spellchecked on every commit by commitlint in a new commit-msg hook, with scopes restricted to the workspace packages. The git hooks are rebalanced at the same time: linting stays at commit time through lint-staged, while pre-push runs only the test suites.
  • Sep 2026 — styling — The frontend drops Sass for plain CSS on PostCSS. The March color-variable refactor had already left the preprocessor supplying only nesting and one breakpoint mixin, so postcss-nested takes over the nesting — the Sass flavour, since BEM &__element concatenation is something the CSS spec cannot express — and the breakpoints become @custom-media definitions that @csstools/postcss-global-data injects into every style block, so components no longer import anything to be responsive.
  • Sep 2026 — pictures — Pictures added by url stop being hotlinked. The backend now downloads them, re-encodes with sharp and stores them alongside file uploads, for post sections and avatars alike, so a third-party host no longer collects the IP of everyone who opens a post, and the encoded size and pixel count are bounded the way an upload's are. Fetching a url the user chose is an SSRF sink, so it is guarded on both sides: addresses reserved for the local network are refused before a socket opens, every name is rechecked at connect time against what it actually resolved to — which is what closes DNS rebinding — and redirects are revalidated hop by hop.
  • Sep 2026 — observability — Backend logging is reworked around structured stdout. Winston had been writing logs/*.log from every cluster worker at once and rotating by bytes per process, so the workers renamed the file out from under each other and lost lines; stdout is now the only transport, Docker's json-file driver owns rotation, and every line is one JSON object carrying a request id, the matched route template and the acting user.
  • Sep 2026 — API contract — The params, query and body of every endpoint are now parsed by a Zod schema before the controller runs, replacing three hand-rolled validator classes and the field checks each controller used to open with. Each route is registered once, together with its schemas, and the OpenAPI 3.1 document is generated from that registration — so the ~2,700 lines of Swagger JSDoc that used to sit above the routes are gone, and the docs cannot describe anything but what the server accepts. A rejected request now names every field that failed instead of only the first.
  • Sep 2026 — infrastructure — Redis joins MongoDB as a second datastore and takes over the two things every request touches: sessions move to connect-redis, and the rate limiter counts through rate-limit-redis instead of an unmaintained Mongo store that shipped its own MongoDB 3 driver and opened a connection pool per limiter per worker. Each limiter also moves under its own key prefix: all five used to share one counter per client, so a single upload could spend a user's read allowance and leave it locked for an hour. A k6 load script in scripts/benchmarks/ measures what the two stores cost per request, which is how the move was checked: roughly half the latency of the MongoDB stores, and about 1.75× the throughput on one box.
  • Sep 2026 — editor — Any post section can be published behind a spoiler: a blurred veil on the rendered post that lifts on click, with the covered content kept out of reach of the keyboard and screen readers until it does. A Write / Preview toggle lands alongside it, rendering the draft through the same component readers see, with voting, routing and tag following switched off since none of them have anything to point at yet. An unfinished post is also kept in the browser it is being written in, saved on a debounce as the typing happens, so a closed tab costs nothing while the copy on the account still carries the draft between devices — the editor opens whichever of the two was written last, and leaving the page with changes the account has not seen asks first.
  • Sep 2026 — linting — ESLint → Oxlint in both packages, rule for rule, dropping the unmaintained airbnb and eslint-plugin-node configs and turning on type-aware linting on the backend. Vue templates are the one thing Oxlint cannot parse, so the frontend keeps a template-only ESLint beside it. The two together run in about half the time of the old single pass, and the backend lints in 3s instead of 17s.

How to Run It

This project can be run in multiple ways, depending on your preferences and setup. Below are the steps for each scenario:

Prerequisites

  • Node.js (>=24.0.0 — see .nvmrc for the exact version used in development)
  • pnpm (>=8.6.0)
  • Docker and Docker Compose (optional, for containerized setups)
  • k6 (optional, for the load benchmarks in scripts/benchmarks/)
  • MongoDB (can be set up locally, remotely, or via Docker)
  • Redis (same — it holds the sessions and the rate limiter counters)

Option 1: Running Without Docker

If you prefer not to use Docker, follow these steps:

  1. Set Up MongoDB:

    • Option A: Local MongoDB
      Install MongoDB locally on your machine and ensure it’s running.
    • Option B: Remote MongoDB (e.g., MongoDB Atlas)
      Use a remote MongoDB instance like MongoDB Atlas. Copy the connection string provided by the service.
  2. Set Up Redis:

    Run one locally, or start a container:

    docker run -d -p 6379:6379 --name smiler-redis redis:8.2-alpine
  3. Configure Environment Variables:

    • Rename .env.example to .env in the root folder.
    • Open the .env file and fill in the required values:
      • For Local MongoDB: Set DB_URL to mongodb://localhost:27017/smiler.
      • For Remote MongoDB: Set DB_URL to the connection string provided by your remote MongoDB service.
      • Set REDIS_URL to redis://localhost:6379, or to wherever your Redis is.
  4. Install Dependencies:

    pnpm install
  5. Run the Application:

    pnpm dev

Option 2: Running With Docker

If you prefer to use Docker, follow these steps:

  1. Set Up MongoDB:

    • Option A: Use Docker to Run MongoDB
      Run a MongoDB container using Docker:

      docker run -d -v /usr/src/smiler/db:/data/db -p 27017:27017 --name smiler-mongo mongo:8.0.29

      Update the DB_URL in .env to mongodb://smiler-mongo:27017/smiler.

    • Option B: Use Remote MongoDB (e.g., MongoDB Atlas)
      Use a remote MongoDB instance like MongoDB Atlas. Copy the connection string and update the DB_URL in .env.

  2. Set Up Redis:

    docker run -d -p 6379:6379 --name smiler-redis redis:8.2-alpine

    Update the REDIS_URL in .env to redis://smiler-redis:6379.

  3. Configure Environment Variables:

    • Rename .env.example to .env in the root folder.
    • Open the .env file and fill in the required values.
  4. Build Images:

    • Build the images using the following commands:
    docker build --target frontend -t <your_username>/smiler-frontend:latest .
    docker build --target backend -t <your_username>/smiler-backend:latest .
  5. Run the Application Images with Docker:

    • Run the images using the following commands:
    docker run -d -p 8080:80 --name smiler-frontend <your_username>/smiler-frontend:latest
    docker run -d -p 3000:3000 --name smiler-backend <your_username>/smiler-backend:latest

Option 3: Running With Docker Compose (All-in-One)

If you want to run both the application and MongoDB using Docker Compose, follow these steps:

  1. Configure Environment Variables:

    • Rename .env.example to .env in the root folder.
    • Open the .env file and fill in the required values. For MongoDB, set DB_URL to mongodb://mongo:27017/smiler. Compose runs its own Redis and points the backend at it, so REDIS_URL is not read in this setup.
  2. Run Docker Compose:

    • Use the provided docker-compose.yml and docker-compose.local.yml files to start the application and MongoDB together:
      # Optionally add --build to build images instead of pulling them from Docker Hub
      docker compose -f docker-compose.yml -f docker-compose.local.yml up -d

Contribution

Feel free to check out the code, open issues if you find bugs, or suggest improvements. Pull requests are welcome too.

Linting

pnpm lint runs every check the pre-commit hook runs, over the whole repo. JavaScript and TypeScript are linted by Oxlint in both packages (.oxlintrc.json), type-aware on the backend.

The frontend runs two linters, because Oxlint reads only the <script> of a .vue file and cannot parse its template:

  • Oxlint lints all the frontend code, <script> blocks included.
  • ESLint (packages/frontend/eslint.config.mjs) lints only the .vue templates, with eslint-plugin-vue and eslint-plugin-vuejs-accessibility. Every rule Oxlint already covers is switched off there, so nothing is reported twice.

Together they still finish in about half the time the single ESLint pass used to take. A new rule for script code belongs in .oxlintrc.json, and one for templates in eslint.config.mjs. Suppression comments follow the same split: oxlint-disable inside <script>, and <!-- eslint-disable --> in templates. Oxlint also honours eslint-disable comments in plain .ts files.

License

This project is licensed under the MIT License.

About

Smiler is my own MEVN (MongoDB, Express, Vue.js, Node.js) site similar to reddit.com or 9gag.com (mostly takes many known features) with many different and awesome features, open Swagger API docs, tests, interesting tools and more. Main reason of making this site is fun and learning new things while making it

Topics

Resources

Stars

12 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages