SnipURL is a NestJS backend for creating short links.
The project currently supports user authentication, creating short links, listing links owned by the current user, public redirects by short code, and click tracking.
- NestJS
- TypeScript
- PostgreSQL
- Prisma
- JWT access and refresh tokens
- Passport JWT
- bcryptjs
- nanoid
- Zod
- Docker Compose for local Postgres and Redis
Redis is included in local infrastructure and dependencies, but is not used by the application yet.
- Register and login with email/password.
- Return access tokens in JSON responses.
- Store refresh tokens in
httpOnlycookies and keep hashed refresh tokens in the database. - Refresh access tokens with the refresh cookie.
- Logout by clearing the stored refresh token and refresh cookie.
- Create short links for authenticated users.
- Generate unique short codes with retry on unique constraint collision.
- Set link expiration to one year from creation.
- List links created by the authenticated user.
- Public redirect by short code.
- Track clicks with IP and user agent.
- Return
404for missing or inactive links. - Return
410for expired links.
src/
auth/ Authentication, JWT strategy, guards, auth endpoints
common/ Shared decorators and types
links/ Authenticated link creation and link listing
prisma/ Prisma client provider
redirect/ Public short-code redirect flow
prisma/
schema.prisma
migrations/
Create a .env file in the project root:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/snipurl"
JWT_SECRET="replace-with-a-long-random-secret"
JWT_EXPIRES_IN="15m"
JWT_REFRESH_SECRET="replace-with-another-long-random-secret"
JWT_REFRESH_EXPIRES_IN="7d"
APP_URL="http://localhost:3000"
FRONTEND_URL="http://localhost:5173"APP_URL is used to build returned short URLs, for example http://localhost:3000/abc1234.
Environment variables are validated when the application starts. Missing or invalid required values will stop the app before it accepts requests. Validation is implemented with Zod.
Install dependencies:
pnpm installStart local infrastructure:
docker compose up -dRun database migrations:
pnpm exec prisma migrate devStart the API in development mode:
pnpm run start:devThe API runs on:
http://localhost:3000
Swagger documentation is available at:
http://localhost:3000/docs
Health check:
http://localhost:3000/health
pnpm run start:dev # start Nest in watch mode
pnpm run build # build the app
pnpm run lint # run ESLint with fixes
pnpm run test # run unit tests
pnpm run db:studio # open Prisma StudioPOST /auth/registercurl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{
"email": "example@example.com",
"password": "example"
}'Response:
{
"accessToken": "..."
}The refresh token is stored in an httpOnly cookie.
POST /auth/logincurl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{
"email": "example@example.com",
"password": "example"
}'POST /auth/refreshcurl -X POST http://localhost:3000/auth/refresh \
-b cookies.txt \
-c cookies.txtIn browser clients, send this request with credentials enabled so the refresh cookie is included.
POST /auth/logout
Authorization: Bearer <accessToken>curl -X POST http://localhost:3000/auth/logout \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"POST /links
Authorization: Bearer <accessToken>curl -X POST http://localhost:3000/links \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-d '{
"originalUrl": "https://example.com/some/long/page"
}'Response:
{
"id": "...",
"shortUrl": "http://localhost:3000/abc1234"
}GET /links
Authorization: Bearer <accessToken>curl -X GET http://localhost:3000/links \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"Response:
[
{
"id": "...",
"originalUrl": "https://example.com/some/long/page",
"shortUrl": "http://localhost:3000/abc1234",
"isActive": true,
"expiresAt": "2027-05-02T19:00:00.000Z",
"createdAt": "2026-05-02T19:00:00.000Z"
}
]GET /:shortCodeExample:
curl -i http://localhost:3000/abc1234Behavior:
- redirects to the original URL when the link is active and not expired;
- stores a click record;
- returns
404 Not Foundwhen the link does not exist or is inactive; - returns
410 Gonewhen the link is expired.
The database has three main models:
User: account, password hash, hashed refresh token.Link: original URL, unique short code, active flag, expiration date, owner.Click: redirect event with IP, user agent, optional country, and timestamp.
countryis not populated yet. It can be added later through GeoIP or platform/CDN headers after deployment.- Redis is not used yet. It can later support rate limiting, caching, queues, or analytics buffering.
- Tests are not implemented yet.
- Planned deployment target: Fly.io for the API, Neon for Postgres, Upstash for Redis, and Vercel for the frontend.