A full-stack blogging platform with a React frontend and an Express/TypeScript API. Write, publish, and read posts, with likes, bookmarks, view tracking, and per-user analytics.
Live: blog-platform-one-sable.vercel.app (frontend) · blog-api-elp7.onrender.com (API)
Frontend (blog-frontend)
- React 18 + TypeScript
- Vite
- Tailwind CSS
- React Router
- react-markdown
Backend (blog-api)
- Node.js + Express 5 + TypeScript
- JWT-based authentication (
jsonwebtoken,bcryptjs) - JSON file storage (no external database required)
- Jest + Supertest for testing
Blog Platform/
├── blog-frontend/ # React app (Vite)
└── blog-api/ # Express API
- Node.js 18+
- npm
cd blog-api
npm install
npm run devThe API starts on http://localhost:3000 by default.
Optional environment variables:
| Variable | Description | Default |
|---|---|---|
PORT |
Port the server listens on | 3000 |
JWT_SECRET |
Secret used to sign auth tokens | random (regenerated on boot) |
POSTS_DATA_FILE |
Path to the posts JSON store | blog-api/data/posts.json |
USERS_DATA_FILE |
Path to the users JSON store | blog-api/data/users.json |
Set JWT_SECRET explicitly in production so tokens remain valid across restarts.
cd blog-frontend
npm install
npm run devThe app starts on http://localhost:5173 (Vite default) and expects the API URL in .env:
VITE_API_BASE_URL=http://localhost:3000
blog-frontend
| Command | Description |
|---|---|
npm run dev |
Start the Vite dev server |
npm run build |
Type-check and build for production |
npm run preview |
Preview the production build |
npm run lint |
Run ESLint |
npm run typecheck |
Run TypeScript in check-only mode |
blog-api
| Command | Description |
|---|---|
npm run dev |
Start the API with hot reload (ts-node-dev) |
npm run build |
Compile TypeScript to dist/ |
npm start |
Run the compiled server (dist/server.js) |
npm test |
Run the Jest test suite |
- Frontend is deployed on Vercel from
blog-frontend(Root Directory set toblog-frontend; seeblog-frontend/vercel.json). ItsVITE_API_BASE_URLenv var points at the Render API URL and is baked in at build time, so it must be redeployed after changing. - Backend is deployed on Render as a free web service from
blog-api(Root Directory set toblog-api; seerender.yaml). ItsFRONTEND_ORIGINSenv var is set to the Vercel URL so CORS only accepts requests from the deployed frontend. - The free Render plan has no persistent disk, so
data/*.jsonresets on every redeploy/restart — signups and posts made in production won't survive a redeploy. See the commented-out disk config inrender.yamlfor how to add persistence on a paid plan.
Base path: /api
Auth (/api/auth)
| Method | Endpoint | Description |
|---|---|---|
| POST | /signup |
Create a new account |
| POST | /login |
Log in and get a JWT |
Posts (/api/posts, all routes require a Bearer token)
| Method | Endpoint | Description |
|---|---|---|
| GET | / |
List all posts |
| GET | /me/bookmarks |
List the current user's bookmarks |
| GET | /:id |
Get a single post |
| POST | / |
Create a post |
| PUT | /:id |
Update a post |
| DELETE | /:id |
Delete a post |
| POST | /:id/view |
Record a view |
| POST | /:id/like |
Toggle a like |
| POST | /:id/bookmark |
Toggle a bookmark |
- Email/password signup and login with JWT sessions
- Create, edit, publish, and delete posts with a markdown editor
- Likes, bookmarks, and view counts per post
- Per-author analytics dashboard
- Responsive landing page, post listing, and reading views
Made by Komal Goel — GitHub · LinkedIn