1- import os
2- import re
31import traceback
4- from datetime import datetime , timezone
5- from typing import TYPE_CHECKING
6-
7- from fastapi import APIRouter , FastAPI , Request
8- from fastapi .responses import HTMLResponse , JSONResponse
9- from pydantic import BaseModel , Field
10-
11- if TYPE_CHECKING :
12- from pymongo .errors import PyMongoError
13- else :
14- try :
15- from pymongo .errors import PyMongoError
16- except ImportError :
17-
18- class PyMongoError (Exception ):
19- pass
20-
2+ from fastapi import FastAPI , Request
3+ from fastapi .responses import JSONResponse
214
225from app import __version__
23- from app .utils import db
24- from app .utils .cache import get_short_from_cache , set_cache_pair
25- from app .utils .helper import generate_code , is_valid_url , sanitize_url
26-
27- SHORT_CODE_PATTERN = re .compile (r"^[A-Za-z0-9]{6}$" )
28- MAX_URL_LENGTH = 2048
6+ from app .routes import api_router , ui_router
297
308app = FastAPI (
319 title = "Tiny API" ,
3210 version = __version__ ,
3311 description = "Tiny URL Shortener API built with FastAPI" ,
12+ docs_url = "/docs" ,
13+ redoc_url = "/redoc" ,
14+ openapi_url = "/openapi.json" ,
3415)
3516
36- api_v1 = APIRouter (prefix = os .getenv ("API_VERSION" , "/api/v1" ), tags = ["v1" ])
37-
3817
3918@app .exception_handler (Exception )
4019async def global_exception_handler (request : Request , exc : Exception ):
@@ -45,167 +24,5 @@ async def global_exception_handler(request: Request, exc: Exception):
4524 )
4625
4726
48- class ShortenRequest (BaseModel ):
49- url : str = Field (..., examples = ["https://abcdkbd.com" ])
50-
51-
52- class ShortenResponse (BaseModel ):
53- success : bool = True
54- input_url : str
55- short_code : str
56- created_on : datetime
57-
58-
59- class ErrorResponse (BaseModel ):
60- success : bool = False
61- error : str
62- input_url : str
63- message : str
64-
65-
66- class VersionResponse (BaseModel ):
67- version : str
68-
69-
70- # -------------------------------------------------
71- # Home
72- # -------------------------------------------------
73- @app .get ("/" , response_class = HTMLResponse , tags = ["Home" ])
74- async def read_root (_ : Request ):
75- return """
76- <html>
77- <head>
78- <title>🌙 tiny API 🌙</title>
79- <style>
80- body {
81- margin: 0;
82- height: 100vh;
83- display: flex;
84- align-items: center;
85- justify-content: center;
86- background: linear-gradient(180deg, #0b1220, #050b14);
87- font-family: "Poppins", system-ui, Arial, sans-serif;
88- color: #f8fafc;
89- }
90- .card {
91- background: rgba(255, 255, 255, 0.06);
92- backdrop-filter: blur(12px);
93- border-radius: 16px;
94- padding: 50px 40px;
95- text-align: center;
96- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
97- max-width: 520px;
98- width: 90%;
99- }
100- h1 {
101- font-size: 2.8em;
102- margin-bottom: 12px;
103- background: linear-gradient(90deg, #5ab9ff, #4cb39f);
104- -webkit-background-clip: text;
105- -webkit-text-fill-color: transparent;
106- }
107- p {
108- font-size: 1.1em;
109- color: #cbd5e1;
110- margin-bottom: 30px;
111- }
112- a {
113- display: inline-block;
114- padding: 14px 26px;
115- border-radius: 12px;
116- background: linear-gradient(90deg, #4cb39f, #5ab9ff);
117- color: #fff;
118- text-decoration: none;
119- font-weight: 700;
120- }
121- </style>
122- </head>
123- <body>
124- <div class="card">
125- <h1>🚀 tiny API</h1>
126- <p>FastAPI backend for the Tiny URL shortener</p>
127- <a href="/docs">View API Documentation</a>
128- </div>
129- </body>
130- </html>
131- """
132-
133-
134- @api_v1 .post ("/shorten" , response_model = ShortenResponse , status_code = 201 )
135- def shorten_url (payload : ShortenRequest ):
136- print (" SHORTEN ENDPOINT HIT " , payload .url )
137- raw_url = payload .url .strip ()
138-
139- if len (raw_url ) > MAX_URL_LENGTH :
140- return JSONResponse (
141- status_code = 413 , content = {"success" : False , "input_url" : payload .url }
142- )
143-
144- original_url = sanitize_url (raw_url )
145-
146- if not is_valid_url (original_url ):
147- return JSONResponse (
148- status_code = 400 ,
149- content = {
150- "success" : False ,
151- "error" : "INVALID_URL" ,
152- "input_url" : payload .url ,
153- "message" : "Invalid URL" ,
154- },
155- )
156-
157- if db .collection is None :
158- cached_short = get_short_from_cache (original_url )
159- short_code = cached_short or generate_code ()
160- set_cache_pair (short_code , original_url )
161- return {
162- "success" : True ,
163- "input_url" : original_url ,
164- "short_code" : short_code ,
165- "created_on" : datetime .now (timezone .utc ),
166- }
167-
168- try :
169- existing = db .collection .find_one ({"original_url" : original_url })
170- except PyMongoError :
171- existing = None
172-
173- if existing :
174- return {
175- "success" : True ,
176- "input_url" : original_url ,
177- "short_code" : existing ["short_code" ],
178- "created_on" : existing ["created_at" ],
179- }
180-
181- short_code = generate_code ()
182- try :
183- db .collection .insert_one (
184- {
185- "short_code" : short_code ,
186- "original_url" : original_url ,
187- "created_at" : datetime .now (timezone .utc ),
188- }
189- )
190- except PyMongoError :
191- pass
192-
193- return {
194- "success" : True ,
195- "input_url" : original_url ,
196- "short_code" : short_code ,
197- "created_on" : datetime .now (timezone .utc ),
198- }
199-
200-
201- @app .get ("/version" )
202- def api_version ():
203- return {"version" : __version__ }
204-
205-
206- @api_v1 .get ("/help" )
207- def get_help ():
208- return {"message" : "Welcome to Tiny API. Visit /docs for API documentation." }
209-
210-
211- app .include_router (api_v1 )
27+ app .include_router (api_router )
28+ app .include_router (ui_router )
0 commit comments