A modern, full-stack URL shortening service built with React, Express, PostgreSQL, and Redis. No accounts required. No tracking. Links are session-bound to your browser.
tinyr demonstrates a complete, production-ready full-stack application designed with portfolio showcase in mind. The project emphasizes:
- Privacy by Design: Anonymous visitor tracking via session cookies—no user accounts or persistent tracking
- Performance: Multi-layer caching (Redis + Database) for instant redirects
- Scalability: Rate limiting, deduplication, and optimized database queries
- Type Safety: Shared TypeScript types across frontend and backend eliminate API contract bugs
- Modern Architecture: Monorepo structure with clear separation of concerns
Users paste a long URL → tinyr generates a 7-character short code → short code redirects to original URL. That's it. No sign-up, no tracking, no noise.
- React 19 – Modern UI framework with hooks and concurrent rendering
- TypeScript 5 – Type-safe component and state management
- Vite 7 – Lightning-fast development server and build tool
- TailwindCSS 4 – Utility-first CSS framework
- shadcn/ui – Accessible, composable component library (Radix UI + CVA)
- TanStack React Query 5 – Server state management with automatic caching/refetching
- Sonner – Toast notifications for UX feedback
- Lucide React – Icon library
- Express.js 4 – Minimal, fast Node.js web framework
- TypeScript 5 – Type-safe server logic
- Drizzle ORM 0.45 – Type-safe SQL query builder with migrations
- PostgreSQL – NeonDB, relational database
- Redis 5 – In-memory cache and rate limiting store
- Cookie-Parser – Secure session cookie handling
- Monorepo (Yarn/npm Workspaces) – Shared types across api/web/shared
- Drizzle Migrations – Version-controlled schema changes
- Docker-ready – Can be containerized for production deployment
┌─────────────────────────────────────────────────────────────┐
│ Frontend (React + Vite) │
│ http://localhost:5173 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • Create short links (POST /api/links) │ │
│ │ • View recent links (GET /api/links/recent) │ │
│ │ • Delete links (DELETE /api/links/:id) │ │
│ │ • Session info (GET /api/session) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↕
HTTPS / CORS / Cookies
↕
┌─────────────────────────────────────────────────────────────┐
│ Backend (Express + TypeScript) │
│ http://localhost:3000 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ • Link shortening logic │ │
│ │ • Code generation & deduplication │ │
│ │ • Rate limiting (Redis) │ │
│ │ • Session management (HttpOnly cookies) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↕
┌───────────────────┴───────────────────┐
↓ ↓
PostgreSQL Redis
(Persistent Data) (Cache & Rate Limit)
• links table • URL→code mapping
• Created/deleted links • Code→URL mapping
• Visitor associations • Request counters
url-shortener/
├── apps/
│ ├── api/ # Express backend server
│ │ ├── src/
│ │ │ ├── index.ts # Entry point, route definitions
│ │ │ ├── db/
│ │ │ │ ├── client.ts # Database connection
│ │ │ │ └── schema.ts # Drizzle ORM table definitions
│ │ │ └── lib/
│ │ │ ├── code.ts # Code generation & deduplication
│ │ │ ├── redis.ts # Redis client setup
│ │ │ └── redisKeys.ts # Redis key naming conventions
│ │ ├── drizzle/ # Database migrations (auto-generated)
│ │ ├── drizzle.config.ts # Drizzle configuration
│ │ ├── package.json # api workspace dependencies
│ │ └── tsconfig.json
│ │
│ └── web/ # React frontend (Vite SPA)
│ ├── src/
│ │ ├── main.tsx # React entry point
│ │ ├── App.tsx # Root component
│ │ ├── components/
│ │ │ ├── ShortenLinkForm.tsx # Link creation form
│ │ │ ├── RecentLinksTable.tsx # Link history view
│ │ │ └── ui/ # shadcn/ui components
│ │ ├── pages/
│ │ │ └── Home.tsx
│ │ └── lib/
│ │ ├── api.ts # Fetch-based API client
│ │ ├── env.ts # Environment validation
│ │ └── utils.ts # Utility functions
│ ├── vite.config.ts
│ ├── package.json # web workspace dependencies
│ └── tsconfig.json
│
├── packages/
│ └── shared/ # Shared TypeScript types
│ ├── src/
│ │ └── index.ts # API contract types (routes, payloads)
│ ├── package.json
│ └── tsconfig.json
│
└── package.json # Root workspace configuration
- Shared Types: Frontend and backend import from
@tinyr/sharedfor type-safe API contracts - Single Command:
npm run devstarts both services with live reloading - Unified Deployment: Can build and deploy as single application
- Code Reusability: Utilities, validation logic shared without duplication
Endpoint: POST /api/links
Creating a short link involves:
- Validate URL (must be http/https)
- Check Redis cache for existing code (SHA-256 hash of URL)
- If miss, check database
- If not found, generate new 7-character alphanumeric code (base-62 encoding)
- Store in database + cache
- Return code
Result: Deduplication prevents duplicate codes for the same URL, saving space and ensuring users always see the same short code for a URL they've seen before.
Endpoint: GET /:code
Redirect flow:
- Check Redis cache for
c:{code}→ long URL (24-hour TTL) - Cache miss → Query PostgreSQL
- Cache hit → Return 302 redirect instantly
- Update Redis on database hits
Result: Repeat visits have sub-millisecond latency.
Implementation: Redis-backed sliding window counter
- Key:
rl:links:create:{IP} - Increments on each link creation
- Expires after 60 seconds
- Returns 429 Too Many Requests if exceeded
Result: Prevents abuse and demonstrates knowledge of rate limiting patterns used in production APIs.
Endpoint: GET /api/session
- Generates UUID
visitorIdon first visit - Stored in HttpOnly, SameSite=Lax cookie
- All links associated with visitor UUID
- Users see only their own links
Security: HttpOnly prevents JavaScript access; SameSite prevents CSRF.
Endpoints: DELETE /api/links/:id or DELETE /api/links (all)
- Links marked with
deleted=trueinstead of removed - Deleted links still redirect (preserves link lifespan)
GET /api/links/recentfilters out deleted links for UI- Database retained for analytics/auditing
CREATE TABLE links (
id UUID PRIMARY KEY,
visitor_id UUID NOT NULL, -- Anonymous visitor ID
code TEXT NOT NULL UNIQUE, -- Short code (7 chars)
long_url TEXT NOT NULL, -- Original URL
created_at TIMESTAMP WITH TIME ZONE,
deleted BOOLEAN DEFAULT false -- Soft delete flag
);
-- Indexes for query performance
CREATE INDEX idx_links_code ON links(code);
CREATE INDEX idx_links_visitor_id ON links(visitor_id);| Decision | Rationale |
|---|---|
UUID for id |
Globally unique, no coordination needed across services |
visitor_id UUID |
Supports anonymous sessions without user table |
code UNIQUE index |
Fast code lookups; prevents duplicates at DB level |
| Soft deletes | Preserves link history; allows "undo" features; auditable |
| No user table | Simplifies schema; privacy-first (no persistent accounts) |
Database schema managed with Drizzle Migrations (apps/api/drizzle/). Each migration is auto-generated and version-controlled:
npm run db:generate # Auto-generate new migration
npm run db:migrate # Apply to database- Node.js 18+ (with npm or yarn)
- PostgreSQL 14+ (local or cloud)
- Redis 6+ (local or cloud)
# Clone repository
git clone https://github.com/jingavin/short-url.git
cd url-shortener
# Install dependencies (all workspaces)
npm install
# Create .env files
cp apps/api/.env.example apps/api/.env
cp apps/web/.env.example apps/web/.envapps/api/.env
DATABASE_URL=postgresql://user:password@localhost:5432/tinyr
REDIS_URL=redis://localhost:6379
NODE_ENV=development
PORT=3000
FRONTEND_URL=http://localhost:5173apps/web/.env
VITE_API_URL=http://localhost:3000# Generate + apply migrations
npm run db:migrate
# Optional: Open Drizzle Studio (visual database editor)
npm run db:studio# Start both frontend (Vite) and backend (tsx watch)
npm run dev
# Or individually:
npm run dev:web # Frontend only (http://localhost:5173)
npm run dev:api # Backend only (http://localhost:3000)npm run build # Builds both frontend and backend
npm run start # Runs production build- Complete frontend-to-database implementation
- Frontend (React, state management, UI)
- Backend (REST API, middleware, business logic)
- Database (schema design, migrations, indexing)
- Infrastructure (caching, rate limiting)
- React 19 with hooks and concurrent features
- TypeScript for type safety across boundaries
- Vite for fast local development
- Drizzle ORM for type-safe database queries
- shadcn/ui for composable, accessible components
- Rate limiting (prevents abuse)
- Multi-layer caching (performance)
- Soft deletes (data preservation)
- Input validation (security)
- CORS configuration (security)
- HttpOnly cookies (session security)
- Error handling (graceful UX)
- Database migrations (version control)
- Redis caching reduces database load
- Deduplication eliminates redundant codes
- Efficient database indexes for fast lookups
- Vite's instant HMR for developer experience
- TypeScript throughout (no
anytypes) - Shared type definitions prevent API contract mismatches
- ESLint configuration for consistency
- Component composition with shadcn/ui
- Monorepo structure with shared dependencies
- Concurrent dev scripts (one command, everything runs)
- Drizzle Studio for visual database inspection
- React Query for automatic cache invalidation
tinyr/
├── apps/
│ ├── api # Express backend (Node.js)
│ │ ├── drizzle/ # Database migrations
│ │ └── src/ # TypeScript source
│ │
│ └── web # React frontend (SPA)
│ └── src/ # TypeScript + React JSX
│
├── packages/
│ └── shared # Shared TypeScript types
│
└── README.md # This file
| File | Purpose |
|---|---|
| apps/api/src/index.ts | API route definitions and middleware setup |
| apps/api/src/db/schema.ts | Database table definitions (Drizzle ORM) |
| apps/api/src/lib/code.ts | Code generation and deduplication logic |
| apps/api/src/lib/redis.ts | Redis client and caching utilities |
| apps/web/src/App.tsx | Root React component and page layout |
| apps/web/src/lib/api.ts | Fetch-based API client with React Query |
| packages/shared/src/index.ts | TypeScript type definitions for API contracts |