Skip to content

Commit 36421ff

Browse files
committed
Updated documentation
1 parent e5e8f05 commit 36421ff

File tree

3 files changed

+365
-1
lines changed

3 files changed

+365
-1
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
spring.application.name=urlshortner
22
logging.level.org.springframework.web=DEBUG
3-
slug.length=10
3+
slug.length=11

Readme.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# URL Shortener Take-Home Exercise (Fullstack)
2+
3+
## Instructions
4+
5+
Your task is to create a URL shortener web application (similar to [bitly](https://bitly.com/) or [TinyURL](https://tinyurl.com/)). This exercise is intentionally open-ended, and you are welcome to implement your solution using the language and tech stack of your choice. If you are familiar with React & Next.js, please use those for your submission. The core functionality of the application should be expressed through your own original code.
6+
7+
You should aim to spend no more than 2 hours on this project. If you don't complete everything in 2 hours, please submit what you have - we value your time and want to see your prioritization skills.
8+
9+
### Application Description
10+
11+
At the root path (e.g., http://localhost:8080/), a user should be presented with a form that allows them to input a URL. When a user submits that form, it should convert the input URL to a shortened version and present that to the user.
12+
13+
The shortened URL should be in the format: http://localhost:8080/{slug}, where `{slug}` is a unique identifier for the original URL.
14+
15+
When a user navigates to the shortened URL, they should be redirected to the original URL that was used to generate this shortened URL.
16+
17+
### Minimum Requirements
18+
19+
* Format and method of generating slugs for shortened URLs are up to you
20+
* Shortened URLs do not need to persist across server shutdown/startup (i.e., setting up a DB isn't necessary - server memory should suffice)
21+
* Only allow valid http(s) URLs
22+
23+
If you have additional time, consider spending it on testing or UI improvements as opposed to supplemental features.
24+
25+
## Evaluation Criteria
26+
27+
We will be evaluating your submission based on the following:
28+
29+
1. Functionality: Does the application work as described?
30+
2. Code quality: Is the code clean, well-organized, and following best practices?
31+
3. Error handling: How does the application handle invalid inputs or errors?
32+
4. Technical choices: Are the chosen technologies appropriate for the task?
33+
5. Documentation: Is the code well-commented and the README clear?
34+
35+
## Deliverables
36+
37+
Please fill out the sections below in the _README.md_ of your project and submit according to the instructions you received with this project. Your code can be sent as a zip file or a link to a repository containing your project.
38+
39+
---
40+
41+
## Implementation Details
42+
### Choosing Technologies
43+
44+
#### Frontend
45+
1. The choice for frontend was going to be React.js because it allows us to build interfaces in form of reusable components, providing more flexibility and state management capabilities.
46+
2. I chose Bootstrap 5 for styling because it is easy to use and because I've used it the most in the past.
47+
3. Instead of using the fetch API, I chose Axios for making API requests because it provides abstraction over the fetch API which makes hanlding requests easier and the code more scalable and maintainable.
48+
49+
#### Backend
50+
1. For the frontend I had to decide between Node.js and Spring Boot. While node.js is a good choice for building RESTful APIs quickly, I decided to go with Spring Boot for the following reasons
51+
1. Scalability and Performance: While Node.js can handle a large number of requests through its non-blocking architecture, Spring Boot is is more scalable, performant and provides security out of the box which also makes it production ready.
52+
2. Developer Productivity: Spring Boot annotations are powerful and avoid boilerplate code, and Java being an Object oriented language helps to model entites and behaviours easily. While Typescript was an option that I considered but I find Java to be more robust and easier to work with.
53+
3. Spring boot has a slightly larger initial memory footprint compared to Node.js but I felt that the tradeoff was worth it for long term perspective.
54+
2. The libaray `JNanoId` was used to generate unique slugs for the shortened URLs. It is a Java implementation of NanoId library on NPM which is a tiny, secure unique string ID generator for JavaScript.
55+
1. I states on its page that it is better than standard UUID and it also has a collision probability calculator which I checked for 10 characters and 1M requests / hour and it was 65 days which is decent for this use case. Later I found that for 11 characters and 1M requests / hour the collision probability is 1 year which is even better.
56+
2. It also provides the flexibility to provide alphabet i.e. the set of characters using which the slug will be generated. I used the default alphabet.
57+
58+
#### Design
59+
1. The application follows a client server architecture.
60+
2. The source code in both sub-projects is organised by feature i.e. `slug` is the feature and all files related to it are present in the `slug` sub-folder in src.
61+
3. The Backend is a REST API which provides the specified endpoints and one additional endpoint
62+
1. `GET /slugs` - To retrieve the list of all slugs and their corresponding URLs.
63+
4. CORS is enabled for all origins, for all methods and headers as of now.
64+
5. The Frontend is a React.js application which has 2 components - `URLShortnerForm` and `SlugList`
65+
1. `URLShortnerForm` is a form which takes the URL input and sends it to the backend to get the shortened URL.
66+
2. `SlugList` is a component which displays the list of all slugs and their corresponding URLs.
67+
6. The service abstraction in both the projects handles the responsibility of hanlding the data and interacting with the persistance layer (in this case the in-memory map in the backend and the session storage in the frontend).
68+
7. I've used sessionStorage instead of localStorage to avoid persisting the data across sessions as our data is anyways in-memory as of now.
69+
70+
71+
#### Other Notes
72+
1. I did do the task in 2 hours but I spent additional 30 minutes (timed) to complete the Slug list component and iron out a few UI bugs.
73+
2. I planned to write unit tests for the backend but could not do so due to time constraints.
74+
3. The documentation is written outside of the 2h time window
75+
4. Co-pilot's inline suggestions were used to autocomplete simple logic and boiler plate code but the core logic was written by me.
76+
77+
78+
79+
#### References
80+
1. [Node.js vs Spring Boot Hello World performance](https://medium.com/deno-the-complete-reference/node-js-vs-springboot-hello-world-performance-comparison-59b4d461526c)
81+
2. [Node.js Fastify vs Spring Boot Webflux performance](https://medium.com/deno-the-complete-reference/node-js-fastify-vs-springboot-webflux-performance-comparison-for-jwt-verify-and-mysql-query-2365f0efb954)
82+
83+
## How to Run
84+
85+
### Prerequisites
86+
87+
- Java 17 or higher
88+
- Node.js 18.x or higher
89+
- npm 10.2.x or higher
90+
91+
### Backend (Spring Boot)
92+
93+
1. Navigate to the `Backend` directory:
94+
95+
```sh
96+
cd Backend
97+
```
98+
99+
2. Build the project using Gradle:
100+
101+
```sh
102+
./gradlew build
103+
```
104+
105+
3. Run the Spring Boot application:
106+
107+
```sh
108+
./gradlew bootRun
109+
```
110+
111+
The backend server should now be running at [http://localhost:8080](http://localhost:8080).
112+
113+
### Frontend (React.js)
114+
115+
1. Navigate to the directory:
116+
117+
```sh
118+
cd ../Frontend
119+
```
120+
121+
2. Install the dependencies:
122+
123+
```sh
124+
npm install
125+
```
126+
127+
3. Start the development server:
128+
129+
```sh
130+
npm run dev
131+
```
132+
133+
The frontend application should now be running at [http://localhost:5173](http://localhost:5173).
134+
135+
### Accessing the Application ([View Demo](https://www.loom.com/share/7bc0a2dd62414bc4a56a92422e876144?sid=517c47ca-9d84-4e9f-badb-323b59dd703d))
136+
137+
- Open your browser and navigate to to access the URL shortener web application.
138+
- You can use the form to input a URL and get a shortened version.
139+
- The shortened URL will be in the format , where `{slug}` is a unique identifier for the original URL.
140+
- When you navigate to the shortened URL, you will be redirected to the original URL.
141+
142+
143+
### Additional Notes
144+
145+
- The backend server runs on port `8080` by default.
146+
- The frontend development server runs on port `5173` by default.
147+
- Ensure that both the backend and frontend servers are running simultaneously to use the application.
148+
149+
## Testing
150+
151+
I have performed manual testing for both the frontend and backend.
152+
153+
For the backend, I've used Postman to test the endpoints. You can import this [Postman collection file](./url-shortner.postman_collection.json) to test the endpoints.
154+
155+
## Tools Used
156+
### IDE
157+
1. VS Code (with copilot)
158+
159+
### Technologies
160+
1. Backend
161+
1. Spring Boot (with Gradle)
162+
1. Spring Web
163+
2. [JNanoId](https://github.com/Soundicly/jnanoid-enhanced) (for generating unique slugs)
164+
2. Java 17
165+
2. Frontend
166+
1. React.js
167+
2. Bootstrap 5 (for styling)
168+
3. Axios (for API requests)
169+
170+
171+
---
172+
173+
Good luck, and we look forward to reviewing your submission!

url-shortner.postman_collection.json

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
{
2+
"info": {
3+
"_postman_id": "85f13f19-cfc8-4d34-9ff4-a63a18df1511",
4+
"name": "url-shortner",
5+
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
6+
"_exporter_id": "11284425"
7+
},
8+
"item": [
9+
{
10+
"name": "create slug",
11+
"event": [
12+
{
13+
"listen": "test",
14+
"script": {
15+
"exec": [
16+
"pm.test(\"Response status code is 201\", function () {",
17+
" pm.response.to.have.status(201);",
18+
"});",
19+
"",
20+
"",
21+
"pm.test(\"Response has the required fields - id and url\", function () {",
22+
" const responseData = pm.response.json();",
23+
" ",
24+
" pm.expect(responseData).to.be.an('object');",
25+
" pm.expect(responseData.id).to.exist;",
26+
" pm.expect(responseData.url).to.exist;",
27+
"});",
28+
"",
29+
"",
30+
"pm.test(\"Id is a non-empty string\", function () {",
31+
" const responseData = pm.response.json();",
32+
" ",
33+
" pm.expect(responseData.id).to.be.a('string').and.to.have.lengthOf.at.least(1, \"Id should be a non-empty string\");",
34+
"});",
35+
"",
36+
"",
37+
"pm.test(\"Url is in a valid format\", function () {",
38+
" const responseData = pm.response.json();",
39+
" ",
40+
" pm.expect(responseData).to.be.an('object');",
41+
" pm.expect(responseData.url).to.match(/^https?:\\/\\/\\w+\\.\\w+/);",
42+
"});",
43+
"",
44+
"",
45+
"pm.test(\"Content-Type header is application/json\", function () {",
46+
" pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");",
47+
"});"
48+
],
49+
"type": "text/javascript"
50+
}
51+
}
52+
],
53+
"request": {
54+
"method": "POST",
55+
"header": [],
56+
"body": {
57+
"mode": "formdata",
58+
"formdata": [
59+
{
60+
"key": "url",
61+
"value": "https://www.linkedin.com/in/jashgopani/",
62+
"type": "text"
63+
}
64+
]
65+
},
66+
"url": {
67+
"raw": "{{BASE_URL}}",
68+
"host": [
69+
"{{BASE_URL}}"
70+
]
71+
}
72+
},
73+
"response": []
74+
},
75+
{
76+
"name": "get slug",
77+
"protocolProfileBehavior": {
78+
"followRedirects": false
79+
},
80+
"request": {
81+
"method": "GET",
82+
"header": [],
83+
"url": {
84+
"raw": "{{BASE_URL}}/U9Y4b3CTAU",
85+
"host": [
86+
"{{BASE_URL}}"
87+
],
88+
"path": [
89+
"U9Y4b3CTAU"
90+
]
91+
}
92+
},
93+
"response": []
94+
},
95+
{
96+
"name": "get all slugs",
97+
"event": [
98+
{
99+
"listen": "test",
100+
"script": {
101+
"exec": [
102+
"pm.test(\"Response status code is 200\", function () {",
103+
" pm.expect(pm.response.code).to.equal(200);",
104+
"});",
105+
"",
106+
"",
107+
"pm.test(\"Response has the required fields - id and url\", function () {",
108+
" const responseData = pm.response.json();",
109+
" ",
110+
" pm.expect(responseData).to.be.an('array');",
111+
" responseData.forEach(function(item) {",
112+
" pm.expect(item).to.be.an('object');",
113+
" pm.expect(item).to.have.property('id');",
114+
" pm.expect(item).to.have.property('url');",
115+
" });",
116+
"});",
117+
"",
118+
"",
119+
"pm.test(\"ID is a non-empty string\", function () {",
120+
" const responseData = pm.response.json();",
121+
" ",
122+
" responseData.forEach(function(item) {",
123+
" pm.expect(item.id).to.be.a('string').and.to.have.lengthOf.at.least(1, \"ID should be a non-empty string\");",
124+
" });",
125+
"});",
126+
"",
127+
"",
128+
"pm.test(\"URL is in a valid format\", function () {",
129+
" const responseData = pm.response.json();",
130+
" ",
131+
" pm.expect(responseData).to.be.an('array');",
132+
" responseData.forEach(function(item) {",
133+
" pm.expect(item.url).to.match(/^https?:\\/\\/(www\\.)?\\w+\\.\\w+/);",
134+
" });",
135+
"});",
136+
"",
137+
"",
138+
"pm.test(\"Content-Type header is application/json\", function () {",
139+
" pm.expect(pm.response.headers.get(\"Content-Type\")).to.include(\"application/json\");",
140+
"});"
141+
],
142+
"type": "text/javascript"
143+
}
144+
}
145+
],
146+
"request": {
147+
"method": "GET",
148+
"header": [],
149+
"url": {
150+
"raw": "{{BASE_URL}}/slugs",
151+
"host": [
152+
"{{BASE_URL}}"
153+
],
154+
"path": [
155+
"slugs"
156+
]
157+
}
158+
},
159+
"response": []
160+
}
161+
],
162+
"event": [
163+
{
164+
"listen": "prerequest",
165+
"script": {
166+
"type": "text/javascript",
167+
"packages": {},
168+
"exec": [
169+
""
170+
]
171+
}
172+
},
173+
{
174+
"listen": "test",
175+
"script": {
176+
"type": "text/javascript",
177+
"packages": {},
178+
"exec": [
179+
""
180+
]
181+
}
182+
}
183+
],
184+
"variable": [
185+
{
186+
"key": "BASE_URL",
187+
"value": "http://localhost:8080",
188+
"type": "string"
189+
}
190+
]
191+
}

0 commit comments

Comments
 (0)