A full-stack URL shortener with secure auth, OAuth login, and click analytics.
Ever sent a URL that looked like this?
https://some-random-site.com/articles/2024/how-to-do-something-very-specific?ref=newsletter&utm_source=email
Yeah. Nobody's clicking that.
LinkMint turns it into something clean, short, and shareable β and tells you exactly how many people actually opened it.
- π Shorten URLs β Paste long link, get short link. Done.
Track clicks β Know if people actually clicked your link.
Email + password auth β Classic signup and login.
OAuth login β One click with Google or GitHub. No new password needed.
HTTP-only cookies β Tokens stored safely, away from JavaScript.
Auto token refresh β You stay logged in. Securely.
Real logout β Token gets blacklisted. None of that fake client-side clear stuff.
Link dashboard β See, manage, and delete all your links in one place.
| Layer | Technology |
|---|---|
| Frontend | React 18 + Vite |
| Backend | Node.js + Express |
| Database | MongoDB + Mongoose |
| Auth | JWT (HTTP-only cookies) + Passport.js (Google & GitHub OAuth) |
| Brevo Transactional API | |
| Hosting | Netlify (frontend) Β· Render (backend) Β· MongoDB Atlas (DB) |
A few deliberate choices worth knowing:
-
Decoupled frontend & backend β They're independent. Scale or update one without touching the other.
-
HTTP-only cookies over localStorage β So JavaScript can never touch your token.
-
Refresh token rotation β Sessions stay alive without weakening security.
-
MongoDB token blacklist β Logout is real. The token is dead the moment you sign out.
-
OAuth β Because nobody wants yet another password to remember.
linkmint/
βββ client/ β React frontend
βββ server/ β Express backend + auth + DB
## βοΈ Setup β Run Locally
**1. Clone the repo**
```bash
git clone https://github.com/your-username/linkmint.git
cd linkmint
2. Install dependencies
# Backend
cd server
npm install
# Frontend (new terminal)
cd ../client
npm install3. Set up environment variables
# Backend
cd server
cp .env.example .env # then open .env and fill in your values
# Frontend
cd ../client
cp .env.example .env # set VITE_API_URL=http://localhost:50004. Start the backend
cd server
npm run dev
# β running at http://localhost:50005. Start the frontend (new terminal)
cd client
npm run dev
# β running at http://localhost:51736. Verify
http://localhost:5000/api/health β { "status": "ok" }
http://localhost:5173 β App loads
See the Production Notes section for which variables to set in Render/Netlify dashboards for deployment.
- Frontend lives on Netlify.
- Backend lives on Render.
- Set all env variables in your host's dashboard β HTTPS is required for cookies and OAuth to work.
- Devs who want to see how a real full-stack auth system is built.
- Learners exploring JWT, OAuth, and cookies in a working project.
- Anyone tired of sharing ugly, untracked links.
| Library | Purpose |
|---|---|
express |
Core web framework β handles routing, middleware, and HTTP requests |
mongoose |
ODM for MongoDB β defines schemas and talks to the database |
nanoid |
Generates unique short IDs for each shortened URL |
dotenv |
Loads environment variables from .env into process.env |
cookie-parser |
Parses HTTP cookies β needed to read JWT tokens from incoming requests |
cors |
Controls which origins can talk to the API β essential for cross-origin frontend/backend setup |
axios |
Makes HTTP requests from the server (e.g. OAuth token exchange) |
| Library | Purpose |
|---|---|
react |
Core UI library β builds the component-based interface |
react-router-dom |
Handles client-side routing between pages (home, login, dashboard, etc.) |
axios |
Makes API calls from the browser to the backend |
react-confetti |
Renders a confetti animation β used on success screens for a fun UX touch |
| Library | Purpose |
|---|---|
jsonwebtoken |
Signs and verifies JWT access and refresh tokens |
bcrypt |
Hashes passwords before storing them β never stored as plain text |
helmet |
Sets secure HTTP headers to protect against common web vulnerabilities |
express-rate-limit |
Limits repeated requests to sensitive routes (login, register, etc.) to prevent brute force |
passport |
Authentication middleware β manages OAuth strategies and session flow |
passport-google-oauth20 |
Google OAuth 2.0 strategy for Passport β handles Google login |
passport-github2 |
GitHub OAuth strategy for Passport β handles GitHub login |
| Service | Purpose |
|---|
LinkMint uses a robust JWT-based authentication system with refresh token rotation and HTTP-only cookies, designed to prevent common vulnerabilities like XSS and CSRF while providing a seamless user experience.
- Token Generation: User submits their email address. The backend generates a secure, random verification token.
- Email Dispatch: The Brevo API sends an email containing a targeted verification link.
- Flow:
User -> Submits Email -> Backend Generates Token -> Email Sent -> User clicks link
- Validation: When the user clicks the emailed link, the backend intercepts the request and validates the token against the database, checking for tampering or expiration.
- Activation: If valid, the user's account is flagged as verified. Unverified accounts cannot log in.
- Completion: Once verified, the user is redirected to a setup screen to create their password.
- Hashing: The assigned password is never saved in plain text. It is securely hashed and salted using
bcryptbefore being persisted to MongoDB.
- Credential Check: User submits email and password. The backend retrieves the user and uses
bcrypt.compareto validate the password hash. - Token Generation: Upon success, the server creates two separate JSON Web Tokens (JWTs):
- Access Token: Short-lived token used for authorizing immediate API requests.
- Refresh Token: Long-lived token used strictly to get new Access Tokens.
- Access Token Delivery: Returned in the JSON response payload and stored in memory by the React frontend.
- Refresh Token Delivery: Attached to the response as a Secure, HTTP-only Cookie.
- Security Consideration: Storing tokens in
localStoragemakes them highly vulnerable to Cross-Site Scripting (XSS). HTTP-only cookies cannot be read by JavaScript, completely mitigating XSS token theft.
- Silent Renewal: When the Access Token expires, the frontend's Axios interceptor automatically hits the
/refreshendpoint, sending the HTTP-only cookie. - Rotation: The backend verifies the old Refresh Token, then issues a new Access Token and a new Refresh Token.
- Security Consideration: The old Refresh Token is immediately invalidated. If a malicious actor manages to steal and use a Refresh Token, the rotation system detects the reuse anomaly and can revoke all sessions for that user.
- Cookie Clearing: The backend instructs the browser to clear the HTTP-only cookie.
- Token Blacklisting: To ensure the token is genuinely invalidated on the server (and not just deleted from the browser), the Refresh Token is added to a MongoDB-backed Token Blacklist. Any future attempt to use that token will be automatically blocked.
- Initiation: User clicks the Google or GitHub login button.
- Redirection: Passport.js redirects the user to the provider for consent.
- Database Sync: The provider sends back the user's profile profile. The backend checks if they exist by provider ID or email. If they are new, an account is created (without a password).
- Token Issuance: The system bypasses the password check, issues the standard Access Token and HTTP-only Refresh Token cookie, and redirects the user into their dashboard.
The backend API is designed around RESTful principles. All responses return a consistent JSON structure.
| Method | Endpoint | Purpose | Body | Response |
|---|---|---|---|---|
POST |
/api/auth/request-verification |
Send verification email | { "email": "user@example.com" } |
{ "message": "Verification email sent." } |
GET |
/api/auth/verify/:token |
Verify email via token | None | Redirects to password setup |
POST |
/api/auth/set-password |
Set password after verification | { "token": "...", "password": "..." } |
{ "message": "Password set. You can now log in." } |
POST |
/api/auth/login |
Log in user | { "email": "...", "password": "..." } |
{ "accessToken": "...", "user": { ... } } (Sets Refresh Cookie) |
POST |
/api/auth/logout |
Log out user | None | { "message": "Logged out" } (Clears Cookie, Blacklists Token) |
POST |
/api/auth/refresh |
Get new Access Token | None (Uses Cookie) | { "accessToken": "..." } (Sets new Refresh Cookie) |
GET |
/api/auth/me |
Get current user profile | None (Requires Bearer token) | { "user": { "id": "...", "email": "..." } } |
| Method | Endpoint | Purpose | Body | Response |
|---|---|---|---|---|
POST |
/api/shorten |
Create a new short URL | { "originalUrl": "https://long-url.com" } |
{ "shortUrl": "linkmint.com/xyz123", "shortCode": "xyz123" } |
GET |
/:shortCode |
Redirect to original URL | None | 302 Redirect to Original URL (Increments click count) |
GET |
/api/stats/:shortCode |
Get analytics for a link | None (Requires Bearer token) | { "originalUrl": "...", "clicks": 42, "createdAt": "..." } |
The LinkMint application is fully deployed and accessible online. The architecture is split to ensure independent scaling and management of the frontend and backend.
- Frontend Hosting: Deployed securely on Netlify. It serves the optimized React (Vite) static build.
- Backend Hosting: Hosted on Render as a Node.js web service.
- Database: Managed via MongoDB Atlas, providing a reliable, cloud-hosted database cluster.
- Connection Logic: The frontend application connects to the backend API via a configured
VITE_API_URLenvironment variable. - Environment Variables: Both Netlify and Render environments are configured with their respective secure credentials (OAuth keys, JWT secrets, database URIs, and Brevo API keys) preventing sensitive data from being exposed in the codebase.
LinkMint is more than just a URL shortener; it serves as a comprehensive demonstration of modern full-stack web development.
Throughout this project, key technical concepts were successfully implemented, including:
- RESTful API Design with clean separation of concerns.
- Advanced Authentication utilizing JWTs alongside HTTP-only cookies to neutralize XSS vectors.
- OAuth Integration for frictionless social logins via Google and GitHub.
- Security Best Practices such as refresh token rotation and database-level token blacklisting to ensure robust logout functionality.
Building this application provided immense learning value in taking a decoupled architecture from local development all the way through to production deployment, tackling real-world challenges in secure session management and environment configuration.