A full-stack URL shortener with click tracking. Paste a long link, get a short one, share it, and watch the clicks come in.
- About
- Features
- Tech Stack
- Architecture
- Project Structure
- Getting Started
- Environment Variables
- Available Scripts
- API Reference
- Data Model
- Known Limitations & Roadmap
- Contributing
- License
- Author
Link-Kit is a URL shortener split into two parts:
- a REST API (Node.js, Express, TypeScript, MongoDB) that creates short links, redirects visitors to the original URL and counts every click, and
- a React single-page app (Vite, Tailwind CSS) where you can shorten links, copy them to your clipboard, see how many times each one has been used, and delete the ones you no longer need.
Each short link is a unique 10-character ID generated with nanoid.
- Shorten any URL. Submit a link and get a unique 10-character short path back.
- Input validation. The API rejects empty values and anything that isn't a valid URL.
- Duplicate protection. Submitting a URL that has already been shortened returns a
409 Conflict. - Redirect and click tracking. Visiting a short link redirects to the original URL and increments its click counter.
- Dashboard. Lists all links, newest first, with the original URL, short path and click count.
- One-click copy. Copy the full short link to your clipboard.
- Delete links. Remove a link directly from the table.
- Typed end to end. TypeScript on both the client and the server.
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite, Tailwind CSS 4, Axios, React Router |
| Backend | Node.js, Express 4, TypeScript, Mongoose 9, nanoid, CORS, dotenv |
| Database | MongoDB (MongoDB Atlas or a local instance) |
| Tooling | ESLint, nodemon, tsx |
┌──────────────────────┐ HTTP / JSON ┌──────────────────────┐ ┌──────────────┐
│ React + Vite (SPA) │ ───────────────────────▶ │ Express REST API │ ─────▶ │ MongoDB │
│ localhost:3000 │ ◀─────────────────────── │ localhost:5001 │ ◀───── │ │
└──────────────────────┘ └──────────────────────┘ └──────────────┘
▲
│ GET /api/v1/short-url/:shortPath
Visitor ───┘ → click +1 → 302 redirect to original URL
LinkKit/
├── backend/
│ ├── src/
│ │ ├── config/
│ │ │ └── db.ts # MongoDB connection
│ │ ├── controllers/
│ │ │ └── url.controller.ts # create / list / redirect / delete handlers
│ │ ├── models/
│ │ │ └── url.model.ts # Mongoose schema
│ │ ├── routes/
│ │ │ └── url.routes.ts # /short-url routes
│ │ ├── app.ts # Express app, middleware, route mounting
│ │ └── server.ts # Entry point (env, DB connect, listen)
│ ├── nodemon.json # Dev-server config (runs src/server.ts via tsx)
│ ├── tsconfig.json
│ └── package.json
│
└── frontend/
├── src/
│ ├── components/
│ │ ├── Header.tsx
│ │ ├── MainContent.tsx # Fetches links, owns reload state
│ │ ├── Form.tsx # "Shorten URL" form
│ │ ├── DataTable.tsx # Links table with copy and delete actions
│ │ └── Footer.tsx
│ ├── helpers/Constants.ts # API base URL from VITE_SERVER_URL
│ ├── interfaces/URLData.ts # Shared URL type
│ ├── App.tsx
│ └── main.tsx
├── vite.config.ts # Dev server on port 3000
└── package.json
- Node.js 18 or newer (the project uses current versions of Vite, React and TypeScript)
- npm
- A MongoDB database: a free MongoDB Atlas cluster or a local MongoDB instance
git clone https://github.com/dk-willing/LinkKit.git
cd LinkKitcd backend
npm installCreate a backend/.env file (see Environment Variables):
PORT=5001
DB_URL=mongodb+srv://<username>:<db_password>@<cluster-host>/linkkit?appName=LinkKit
DB_PASSWORD=your_database_passwordStart the development server:
npm run devThe API will be available at http://localhost:5001. You should see Database connected successfully in the console.
In a second terminal:
cd frontend
npm installCreate a frontend/.env file:
VITE_SERVER_URL=http://localhost:5001/api/v1Start the dev server:
npm run devThe app opens at http://localhost:3000.
Note: The backend only accepts requests from
http://localhost:3000(see Known Limitations). Keep the frontend on that port during development.
| Variable | Required | Description |
|---|---|---|
PORT |
No | Port the API listens on. Defaults to 5001. |
DB_URL |
Yes | MongoDB connection string. Keep the literal <db_password> placeholder, which is replaced at runtime. |
DB_PASSWORD |
Yes | Database password, substituted into DB_URL in place of <db_password>. |
| Variable | Required | Description |
|---|---|---|
VITE_SERVER_URL |
Yes | Base URL of the API, e.g. http://localhost:5001/api/v1. |
⚠️ Never commit.envfiles. Both are already covered by.gitignore. If a real password was ever pushed to a public repo, rotate it immediately.
| Command | Description |
|---|---|
npm run dev |
Start the API with hot reload (nodemon + tsx). |
npm run build |
Compile TypeScript to dist/, then start dist/server.js. |
npm start |
Run the compiled server from dist/server.js. |
| Command | Description |
|---|---|
npm run dev |
Start the Vite dev server on port 3000. |
npm run build |
Type-check and create a production build. |
npm run preview |
Preview the production build locally. |
npm run lint |
Lint the project with ESLint. |
Base URL: http://localhost:5001/api/v1
| Method | Endpoint | Description |
|---|---|---|
POST |
/short-url |
Create a short URL |
GET |
/short-url |
List all short URLs (newest first) |
GET |
/short-url/:id |
Redirect to the original URL and count a click |
DELETE |
/short-url/:id |
Delete a short URL |
The
:idparameter means different things per method: forGETit is theshortPath(e.g.V1StGXR8_Z), and forDELETEit is the document's MongoDB_id.
POST /short-url
curl -X POST http://localhost:5001/api/v1/short-url \
-H "Content-Type: application/json" \
-d '{"fullPath": "https://example.com/some/very/long/link"}'201 Created
{
"status": "success",
"data": {
"url": {
"_id": "66f1b2c3d4e5f6a7b8c9d0e1",
"fullPath": "https://example.com/some/very/long/link",
"shortPath": "V1StGXR8_Z",
"clicks": 0,
"createdAt": "2026-10-05T14:03:00.000Z",
"updatedAt": "2026-10-05T14:03:00.000Z"
}
}
}Errors
| Status | When |
|---|---|
400 |
fullPath is missing, not a string, or not a valid URL |
409 |
The URL has already been shortened |
500 |
Unexpected server error |
GET /short-url
curl http://localhost:5001/api/v1/short-url200 OK
{
"status": "success",
"data": {
"urls": [
{
"_id": "66f1b2c3d4e5f6a7b8c9d0e1",
"fullPath": "https://example.com/some/very/long/link",
"shortPath": "V1StGXR8_Z",
"clicks": 3,
"createdAt": "2026-10-05T14:03:00.000Z",
"updatedAt": "2026-10-05T16:10:00.000Z"
}
]
}
}GET /short-url/:shortPath
curl -i http://localhost:5001/api/v1/short-url/V1StGXR8_ZIncrements the click counter and responds with a 302 redirect to the original URL. Returns 404 if the short path doesn't exist.
DELETE /short-url/:id
curl -X DELETE http://localhost:5001/api/v1/short-url/66f1b2c3d4e5f6a7b8c9d0e1Returns 204 No Content on success.
Stored in the urls collection (Mongoose model URL):
| Field | Type | Notes |
|---|---|---|
_id |
ObjectId | Generated by MongoDB |
fullPath |
String | Required. The original URL. |
shortPath |
String | Required. Auto-generated 10-character nanoid. |
clicks |
Number | Defaults to 0, incremented on every redirect. |
createdAt |
Date | Added automatically (timestamps: true) |
updatedAt |
Date | Added automatically (timestamps: true) |
Link-Kit is a work in progress. Planned improvements:
- Short links that look like short links. The app currently serves redirects from
/api/v1/short-url/:shortPath. Add a top-level/:shortPathredirect route so links are genuinely short. - Configurable CORS. The allowed origin is hard-coded to
http://localhost:3000. Move it to an environment variable (e.g.CLIENT_URL) before deploying. - Database-level uniqueness. Add unique indexes on
fullPathandshortPathso the duplicate-key handling in the controller is backed by the database. - Delete edge cases.
DELETEshould return404when the ID doesn't exist, and204responses should not carry a body. - User-facing error messages. Show validation and duplicate-URL errors in the UI instead of only logging them to the console.
- Authentication. Per-user link lists.
- Analytics. Click timestamps, referrers and per-link charts.
- Custom aliases and expiry dates.
- Automated tests (Jest/Vitest + Supertest) and a CI workflow.
- Deployment guide (Render/Railway for the API, Vercel/Netlify for the frontend).
Contributions, issues and feature requests are welcome.
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Commit your changes:
git commit -m "Add my feature" - Push to the branch:
git push origin feature/my-feature - Open a Pull Request
Distributed under the MIT License.
David Karikari (@dk-willing)