Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Tiny-URL-CD-Pipeline

on:
workflow_run:
workflows: ["Tiny-URL-CI-Pipeline"] # must match `name:` in ci.yml
types:
- completed
workflow_dispatch:

jobs:
build-docker-images:
#if: >
# github.event.workflow_run.conclusion == 'success' &&
#github.event.workflow_run.head_branch == 'main'
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Cache Docker layers
uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-docker-${{ github.sha }}
restore-keys: |
${{ runner.os }}-docker-

- name: Build API Docker image
run: |
docker build \
--file api/Dockerfile \
--tag your-org/tinyurl-api:main \
--cache-from=type=local,src=/tmp/.buildx-cache \
--cache-to=type=local,dest=/tmp/.buildx-cache \
./api

- name: Build Webapp Docker image
run: |
docker build \
--file webapp/Dockerfile \
--tag your-org/tinyurl-webapp:main \
--cache-from=type=local,src=/tmp/.buildx-cache \
--cache-to=type=local,dest=/tmp/.buildx-cache \
./webapp
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Tiny URL CI Pipeline
name: Tiny-URL-CI-Pipeline

on:
push:
Expand Down
5 changes: 5 additions & 0 deletions api/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
dist
.next
.env
*.log
7 changes: 4 additions & 3 deletions api/.env-dev
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
MACHINE_ID=1
PORT=3000
# Perhpas we should only use a domain name here instead of a full URL (with the port as it is error prone)
SHORTENED_BASE_URL=http://localhost:3000
PORT=3001
# This is the base URL for the shortened URLs. It is used to generate the shortened URL and redirect to the original URL.
# Like http://localhost:3001/aZ34PM redirecting to https://google.com
SHORTENED_BASE_URL=http://localhost:3001
# You can set this property to false for rapid testing. It will use in-memory storage rather than pulling Redis and DynamoDB
USE_PERSISTENT_STORAGE=false
REDIS_URL=redis://localhost:6379
Expand Down
17 changes: 17 additions & 0 deletions api/.env-docker
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
NODE_ENV=development
MACHINE_ID=1
PORT=3000
# This is the base URL for the shortened URLs. It is used to generate the shortened URL and redirect to the original URL.
# Like http://localhost:3001/aZ34PM redirecting to https://google.com
SHORTENED_BASE_URL=http://localhost:3001
# You can set this property to false for rapid testing. It will use in-memory storage rather than pulling Redis and DynamoDB
USE_PERSISTENT_STORAGE=true
REDIS_URL=redis://redis:6379
# 10 minutes
REDIS_TTL=600
AWS_REGION=us-east-1
DYNAMO_TABLE=shortUrls
# for local development only
DYNAMO_ENDPOINT=http://dynamodb:8000
AWS_ACCESS_KEY_ID=dummy
AWS_SECRET_ACCESS_KEY=dummy
27 changes: 27 additions & 0 deletions api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Stage 1: Build
FROM node:18-alpine AS builder

WORKDIR /app
COPY . .
RUN yarn install --frozen-lockfile
RUN yarn build

# Stage 2: Production image
FROM node:18-alpine

WORKDIR /app
ENV NODE_ENV=production

# Copy only what is needed for production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
COPY --from=builder /app/yarn.lock ./

RUN yarn install --production --frozen-lockfile

# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

EXPOSE 3000
CMD ["node", "dist/main"]
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ describe('ShortenUrl controller', () => {
const response = await underTest.shortenUrl({ url: longUrl });

// Extract the unique ID part of the URL
const urlPattern = /^http:\/\/localhost:3000\/([A-Za-z0-9]{10})$/;
const baseUrl = process.env.SHORTENED_BASE_URL;
if (!baseUrl) throw new Error('SHORTENED_BASE_URL is not defined');

const escapedBaseUrl = baseUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escape for regex
const urlPattern = new RegExp(`^${escapedBaseUrl}/([A-Za-z0-9]{10})$`);
const match = response?.shortenedUrl.match(urlPattern);

expect(response).not.toBeNull();
Expand Down
30 changes: 27 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
version: '3.8'

services:

api:
build:
context: ./api
dockerfile: Dockerfile
container_name: tinyurl-api
restart: always
ports:
- "3001:3000"
env_file:
- ./api/.env-docker

webapp:
build:
context: ./webapp
dockerfile: Dockerfile
container_name: tinyurl-webapp
ports:
- "3000:3000"
depends_on:
- api
env_file:
- ./webapp/.env-docker

redis:
image: redis:7.2.7
container_name: redis
restart: always
ports:
- "6379:6379"
command: redis-server --save "" --appendonly no --maxmemory 100mb --maxmemory-policy allkeys-lru # Disable disk persistence
command: redis-server --save "" --appendonly no --maxmemory 100mb --maxmemory-policy allkeys-lru # Disable disk persistence
volumes:
- redis_data:/data # ✅ Ensures no persistence to disk
- redis_data:/data
environment:
- ALLOW_EMPTY_PASSWORD=yes

Expand All @@ -22,7 +46,7 @@ services:
depends_on:
- redis
environment:
- REDIS_URI=redis://redis:6379 #Connect RedisInsight to Redis automatically
- REDIS_URI=redis://redis:6379 #Connect RedisInsight to Redis automatically

dynamodb:
image: amazon/dynamodb-local:2.6.0
Expand Down
5 changes: 5 additions & 0 deletions webapp/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
dist
.next
.env
*.log
1 change: 1 addition & 0 deletions webapp/.env-dev
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SHORTENED_BASE_URL=http://localhost:3001
1 change: 1 addition & 0 deletions webapp/.env-docker
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SHORTENED_BASE_URL=http://api:3000
27 changes: 27 additions & 0 deletions webapp/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Stage 1: Build
FROM node:18-alpine AS builder

WORKDIR /app
COPY . .
RUN yarn install --frozen-lockfile
RUN yarn build

# Stage 2: Production image
FROM node:18-alpine

WORKDIR /app
ENV NODE_ENV=production

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/package.json ./
COPY --from=builder /app/yarn.lock ./

RUN yarn install --production --frozen-lockfile

# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

EXPOSE 3000
CMD ["yarn", "start"]
9 changes: 7 additions & 2 deletions webapp/app/api/url-shortener/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@ import { NextRequest, NextResponse } from 'next/server';
import { createShortenUrl } from '../../../lib/create-shorten-url';

export async function POST(request: NextRequest) {
const { url } = await request.json().catch(() => ({})); // ✅ Simplified JSON parsing
const { url } = await request.json().catch(() => ({})); //

if (typeof url !== 'string' || !url.trim()) {
return NextResponse.json({ error: 'Invalid URL format' }, { status: 400 });
}

try {
const shortenedUrl = await createShortenUrl(url, 'http://localhost:3000');
const shortenedBaseUrl = process.env.SHORTENED_BASE_URL;

if (!shortenedBaseUrl) {
throw new Error('SHORTENED_BASE_URL not configured');
}
const shortenedUrl = await createShortenUrl(url, shortenedBaseUrl);
return NextResponse.json({ shortenedUrl }, { status: 200 });
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 400 });
Expand Down
2 changes: 1 addition & 1 deletion webapp/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<title>URL Shortener</title>
<title>TinyURL like application</title>
</head>
<body>
<ThemeProvider theme={theme}>
Expand Down
2 changes: 1 addition & 1 deletion webapp/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"scripts": {
"dev": "next dev",
"start:dev": "next dev",
"build": "next build",
"start": "next start",
"test": "jest --testMatch '**/components/**/*.(test|spec).@(ts|tsx|jsx)'",
Expand Down
Loading