-
Couldn't load subscription status.
- Fork 47
Technical Challenge
Welcome to the Code4Community technical take-home challenge.
Unlike traditional take-home challenges, we aim to be fair to everyone regardless of their prior experiences. Therefore, this "challenge" will function more like a web dev tutorial. We'll walk through the creation of a fully functional web application. With that being said, you'll most likely come across terms or concepts that you won't be familiar with. If anything sounds new, make sure to look them up! There are tons of great resources out there and this is an important skill to have.
For this challenge, you will be building a URL shortener like bit.ly. Users will be able to input any URL they would like, and your service will produce a shorter URL that redirects to the original URL.
Here's an outline of what to expect:
- Project setup
- A basic user interface in React
- Backend logic to store original URLs, and produce shortened URLs
- Enhancing the user interface with a component library
- Refactoring for peristence
- Tests and actual programming
- Epilogue
The project is designed to be finished within one focused session of coding, or over the course of multiple days as needed. There is no time limit to the challenge besides the deadline posted on our website for developer applications (visit the website for more).
The only requirement is that you're learning something new, type out the code provided instead of copy pasting. You'll be able to learn more and absorb information better this way.
Throughout the tutorial I'll link to resources that explain topics in depth if you're unfamiliar. These resources are far better than anything I would write by hand. Refer to them if you get stuck or don't understand something on a conceptual level. (i.e. what is the DOM?)
Code4Community primarily uses Node and Typescript. Our monorepo is managed through a framework called Nx, which makes adding new projects (as you are about to do) very easy!
It also makes setup easy! First go ahead and install Node 18.x here: https://nodejs.org/en/download
Double check you installed Node and npm by installing yarn (another package manager we'll use later).
In your terminal run:
npm install --global yarnYou may need elevated privileges, but otherwise if you did the above steps correctly then this will run fine!
Use your Github account to fork this repo. Forking the repo will create a copy that you own! This will allow you to make changes as needed, without needing our approval to edit.

⚠️ Make sure that at this point, you are working off your forked repo. Not the original.
Then go ahead and clone to repo to your local machine. Use your terminal, or Github Desktop, whatever works best for you.
Open up the repo on your local machine using your interface of choice. If you have no idea what to use, try Visual Studio Code, or JetBrain's Webstorm/IntelliJ.
In the root directory of the repo, run yarn install, which will begin the process of downloading hundreds of Javascript libraries onto your computer! This will take time, so be patient.
Once you finish successfully, you're ready to start building ✅
We're going to set up a new project using Nx, a tool to manage projects and shared dependencies efficiently.
In the root directory of the repo, run the following commands:
yarn global add nx@latest # So that you won't need to prefix your nx commands with yarn all the time!
nx g @nx/react:app url/clientYou will be prompted to fill in details, answer CSS, No, and webpack.
This command just created two new projects:
-
/apps/url/clientis the React app which provides the user interface for our URL shortener -
/apps/url/client-e2eis the Cypress project for end-to-end testing. We'll come back to this one shortly
Run nx serve url-client in the root directory to start the React app, then navigate to http://localhost:4200
As you can see, Nx has a beautiful default welcome page. Let's see the source code. Navigate to /client/src/app/app.tsx and make the following change:
import styles from './app.module.css';
import NxWelcome from './nx-welcome';
export function App() {
return (
<div>
+ <h1>Hello, World!</h1>
- <NxWelcome title="url-client" />
</div>
);
}
export default App;And you should get the much less beautiful page below:
For beginners to HTML and React, we just changed a source file and it automatically updated on our browser! We deleted the existing page by removing the NxWelcome component, and introduced an h1 (Heading 1) tag with text of our own. If you're not familiar with React or want a refresher, watch a video or two on it, like this one.
Our next step is to create a minimally viable UI. Lots of times we care most about getting something functional quickly. A pretty UI is critical when presenting to clients, but it can come later. Our plan will be to wire up all the essential parts of the app to get URL shortening to work properly, then we'll come back to styling at the end.
The most minimal URL shortening UI I could think of was a form with a simple input field and submit button. All the URLs you shortened would then appear below in a bulleted list.
export function App() {
return (
<div>
<h1>My URL Shortener</h1>
<form>
<label>URL</label>
<input placeholder="www.my-super-long-url-here.com/12345" />
<button>Generate</button>
</form>
<ul>
<li>ex.co/abcde - www.example.com/this/is/a/long/slug</li>
</ul>
</div>
);
}
export default App;
The user flow will be as follows:
- A user types in their long URL
- That URL is sent to our backend, which will store and generate a new short URL that redirects to the original
- The frontend adds this new URL to the bulleted list
Since we don't have the backend up and running just yet, we'll emulate this behavior locally with a list of pairs of strings. Each pair will represent the original, and shortened URL.
In Typescript we can encode our data definition as a type! Add the following to the top of the file:
type Shortened = {
original: string;
short: string;
};Now we can hook up the rest of the form together, and save local Shortened input to be displayed back to the user.
To do this we'll need to use React hooks, which efficiently repaint our webpage based on data and events from the user.
One of the most common is useState, which stores local state for that React component. Let's add a state hook for the text input for a quick example:
export function App() {
const [inputUrl, setInputUrl] = useState<string>('');
return (
<div>
<h1>My URL Shortener</h1>
<p>{inputUrl}</p>
<form>
<label>URL</label>
<input
value={inputUrl}
onChange={(e) => {
setInputUrl(e.target.value);
}}
placeholder="www.my-super-long-url-here.com/12345"
/>fuse
<button type="submit">Generate</button>
</form>
...Screen.Recording.2023-05-29.at.10.09.54.PM.mov
The other hook we'll learn about is useCallback, which comes in handy when using higher order functions. You might have noticed that pressing the submit button will reload the page. This isn't ideal! Let's fix that and take a closer look at how the browser really works.
export function App() {
const [inputUrl, setInputUrl] = useState<string>('');
const onSubmit = useCallback(
(event: FormEvent) => {
event.preventDefault();
console.log(event);
},
[]
);
return (
<div>
<h1>My URL Shortener</h1>
<p>{inputUrl}</p>
<form onSubmit={onSubmit}>
...Go ahead and press the button. Open up your developer console and you'll see a Javascript object printed to the console. Go ahead and inspect the object yourself.
Screen.Recording.2023-05-29.at.10.20.20.PM.mov
Objects like these are propagated from elements, which bubble up the element tree (DOM) to an event handler, which determines what to do by either default behavior or a provided callback function.
The default behavior for forms is to POST the data, which causes a page refresh. We don't want to do that so we .preventDefault().
With that, we have all the ingredients we need to produce a minimal UI. With a few more uses of hooks we can get a functional UI:
import { FormEvent, useCallback, useState } from 'react';
type Shortened = {
original: string;
short: string;
};
export function App() {
const [urls, setUrls] = useState<Array<Shortened>>([]);
const [inputUrl, setInputUrl] = useState<string>('');
const onSubmit = useCallback(
(event: FormEvent) => {
event.preventDefault();
const newUrl: Shortened = {
original: inputUrl,
short: 'short.com/123',
};
setUrls([newUrl, ...urls]);
setInputUrl('');
},
[urls, setUrls, inputUrl, setInputUrl]
);
return (
<div>
<h1>My URL Shortener</h1>
<form onSubmit={onSubmit}>
<label>URL</label>
<input
value={inputUrl}
onChange={(e) => {
setInputUrl(e.target.value);
}}
placeholder="www.my-super-long-url-here.com/12345"
/>
<button type="submit">Generate</button>
</form>
<ul>
{urls.map((u) => (
<li>
{u.short} - {u.original}
</li>
))}
</ul>
</div>
);
}
export default App;The final addition is the idiom of using .map to generate a list of elements based on local state. Those of you who remember map and quasiquote should feel pretty comfortable with this pattern!
Screen.Recording.2023-05-29.at.10.25.09.PM.mov
With that our client is nearly done! The only part we have to replace is where we hardcode the shortened URL. Instead, we should delegate to a backend service which will do that for us. That's for the next section...
🛑 STOP! Our code might be functional but our React component is getting large. How might you begin to refactor this into smaller pieces?
Let's start off by creating the new backend project. We'll be using Express which is an older but influential and simple library for creating web servers in Javascript/Typescript. Run the following in the root directory:
nx g @nx/express:app url/server
Now serve the backend!
nx serve url-serverAnd go into your browser and visit http://localhost:3333/api
Unlike the frontend, which communicates with the user via a visual UI (changes to the DOM), the backend will respond (and receive) JSON primarily.
JSON is a data transfer language. That means that it's a language solely for the purpose of encoding structured data in a stringified form to be sent over the wire.
The message, pretty printed in your browser, is represented by the following JSON:
{
"message": "Welcome to url/server!"
}Go to /server/src/main.ts and edit it to see the JSON response change:
import express from 'express';
const app = express();
app.use('/assets', express.static(path.join(__dirname, 'assets')));
app.get('/api', (req, res) => {
+ res.send({ my_new_message: 'Hey there! This is me, writing my own custom content.' });
- res.send({ message: 'Welcome to url/server!' });
});
const port = process.env.PORT || 3333;
const server = app.listen(port, () => {
console.log(`Listening at http://localhost:${port}/api`);
});
server.on('error', console.error);Go back to http://localhost:3333/api to see the changes reflected.
Backend development will feel much more familiar to the ordinary programming you might have learned in class.
Our backend really has one service, its only purpose is to consume a request with a long URL, then respond with a shortened URL. We might also include the original URL in the response for ease of use.
We will install this functionally at the route/endpoint POST /api/shorten.
It will expect JSON of the form:
{
"original": "http://example.com/this-is-a-super-duper-long-url"
}and if successful, will respond with an HTTP status code of 201 with the JSON response of:
{
"short": "http://localhost:3333/s/abc"
"original": "http://example.com/this-is-a-super-duper-long-url"
}Clearly we're going to need some function with the signature LongUrl -> ShortUrl. We'll also need to store this state within the Express application.
Let's use a dumb, but effective method. For a newly issued URL http://localhost:3333/s/<id>, have the <id> be the total number of URLs issued so far. For example, if we have already issued 10 shortened URLs, then the 11th URL would be http://localhost:3333/s/10.
Finally we'll use the Express library to install a new POST endpoint for creating shortened urls, use the built-in JSON parser for POST request bodies, middleware for CORS, and a GET endpoint that redirects to original URLs (the long URLs themselves).
import express from 'express';
import cors from 'cors';
// Mutable Application State
/**
* A map of Short URL IDs to full original URLs
* http://localhost/s/123, http://example.com/...
*
* { 123 -> 'http://example.com/...' }
*/
const urlmap: Record<number, string> = {};
// Actions
/**
* Produces the shortened form of a given URL
* Invariant: url is a valid URL, and does not already exist as a value in urlmap
* Effect: updates the `urlmap` to record the url and its shortened version.
*/
function shortenUrl(url: string): string {
const id = Object.keys(urlmap).length; // number of elements in hash table
const short = `http://localhost:3333/s/${id}`;
urlmap[id] = url;
return short;
}
// App
const app = express();
app.use(express.json());
app.use(cors());
app.post('/api/shorten', (req, res) => {
const original = req.body.original;
const short = shortenUrl(original);
res.send({
short: short,
original: original,
});
});
app.get('/s/:id', (req, res) => {
const id = Number(req.params.id);
const original = urlmap[id];
res.redirect(original);
});
const port = process.env.PORT || 3333;
const server = app.listen(port, () => {
console.log(`Listening at http://localhost:${port}/api`);
});
server.on('error', console.error);🛑 STOP! We have committed multiple mistakes in our design that hamper our ability to test our code. Think. How might you fix that?
Let's try it out... but how are we going to test our POST endpoint? We don't exactly have that ability in our browser's search bar.
There are many options for playing with APIs:
- https://www.postman.com/downloads/
- https://insomnia.rest/download
- https://linuxize.com/post/curl-post-request/
Postman is the most popular within the org, and so we'll use it going forward.
Select POST, enter the shorten endpoint, then enter a valid JSON body, whatever URL you want. Finally, press send!
Clicking on the shortened link should then redirect you to your original url as expected!
Now, back to the frontend.
If you previously stopped the process serving the frontend or would just like to simultaneously run the frontend and backend, then you can use the following handy Nx command:
nx run-many -t serve -p url-client url-serverWe're going to finally link our two components via an HTTP request. To do that nicely, we'll use the axios library. Run the following in the root directory of the repo
yarn add axiosBack in our React component we update the form submit callback to actually make a request to the backend. Make sure both apps are running when you try to use the frontend.
const onSubmit = useCallback(
async (event: FormEvent) => {
event.preventDefault();
const response = await axios.post(`http://localhost:3333/api/shorten`, {
original: inputUrl,
});
const newUrl = response.data as Shortened; // 🚨 This should set off alarm bells in your head! Why?
setUrls([newUrl, ...urls]);
setInputUrl('');
},
[urls, setUrls, inputUrl, setInputUrl]
);🛑 STOP! What would have happened if we had not included
app.use(cors())on the backend? If you don't know, try removing it to see what happens... Read https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS for more.
If you get an Axios Network Error here make sure to check the browser console to see what went wrong and adjust from there.
Otherwise you should get something like below:
Screen.Recording.2023-06-02.at.12.57.05.AM.mov
Congratulations, you just hacked together an MVP! Start that Y-Combinator application and pack your bags for Silicon Valley ⭐
Right now our frontend is functional but ugly.
Our newest projects use Chakra UI. Chakra is a simple and customizable component library.
We can easily add Chakra to our project.
We'll more or less follow the Getting Started page on the website.
Chakra should already be installed. First add the ChakraProvider component near the root of the component tree.
main.tsx is an alright place to put this:
import { StrictMode } from 'react';
import * as ReactDOM from 'react-dom/client';
import { ChakraProvider } from '@chakra-ui/react';
import App from './app/app';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<StrictMode>
<ChakraProvider>
<App />
</ChakraProvider>
</StrictMode>
);Now we can use Chakra. We style our components by passing specific props to Chakra components.
Check out the Chakra docs for more details: https://chakra-ui.com/docs/styled-system/style-props
I'll leave the exact styling up as a creative exercise, but this is what I came up with:
import { FormEvent, useCallback, useState } from 'react';
import axios from 'axios';
import {
Button,
Container,
Text,
Input,
UnorderedList,
ListItem,
Link,
} from '@chakra-ui/react';
type Shortened = {
original: string;
short: string;
};
export function App() {
const [urls, setUrls] = useState<Array<Shortened>>([]);
const [inputUrl, setInputUrl] = useState<string>('');
const onSubmit = useCallback(
async (event: FormEvent) => {
event.preventDefault();
const response = await axios.post(`http://localhost:3333/api/shorten`, {
original: inputUrl,
});
const newUrl = response.data as Shortened;
setUrls([newUrl, ...urls]);
setInputUrl('');
},
[urls, setUrls, inputUrl, setInputUrl]
);
return (
<Container maxWidth="4xl" marginBlock={10} textAlign="center">
<Text fontSize="4xl">My URL Shortener</Text>
<form onSubmit={onSubmit}>
<Input
size="lg"
marginBlock={4}
value={inputUrl}
onChange={(e) => {
setInputUrl(e.target.value);
}}
placeholder="www.my-super-long-url-here.com/12345"
/>
<Button type="submit" colorScheme="teal" size="lg">
Generate
</Button>
</form>
<UnorderedList textAlign="left">
{urls.map((u) => (
<ListItem>
<Link href={u.short} color="teal.500">
{u.short}
</Link>{' '}
- {u.original}
</ListItem>
))}
</UnorderedList>
</Container>
);
}
export default App;Go ahead and add your own flare! This is just an example.
Chakra UI also has quite a few utilities for client side input validation. You might have noticed that we haven't validated any input.
🛑 STOP! Is client side validation sufficient?
Since we enhanced our frontend let's do the same with our backend. It sucks that our server has to store all these URLs in-memory. Every time the server restarts we lose all our lovely links!
If we wanted to deploy this to production, we would need persistence. URLs aren't that important, but could you imagine losing customer data like financial grants due to a server restart? Yikes.
We usually use Dynamo or PostgreSQL in production, but for simplicity we'll use SQLite, which is a RDMS that runs locally on your machine. No crazy installation required.
Download SQLite here: https://www.sqlite.org/download.html
And download the Node bindings so we can interact with SQLite from our Typescript backend.
yarn add sqlite3
yarn add sqliteWe need both libraries unfortunately.
Let's look at the docs: https://www.npmjs.com/package/sqlite#usage
Similar to how our frontend and backend are distinct and independent components of our software system, our persistent storage is too. We communicate to this system through a special Domain Specific Language (DSL) called SQL. Here are the SQL statements we'll be using for our project:
-- We need to create a table named "url" to replace our in-memory equivalent. It just has an id and the original URL.
CREATE TABLE IF NOT EXISTS url (id INTEGER PRIMARY KEY AUTOINCREMENT, original TEXT);
-- When we create a new URL we need to insert a record into the table. The ID will be generated automatically, so we only need the original URL.
INSERT INTO url (original) VALUES (?);
-- Finally, when retrieving a URL by its short ID, we need to select from the table matching that ID.
SELECT original FROM url WHERE id = (?);Adding this external component will require our backend to "wait" on a connection to be formed with the database. While our database is in our own file system, we follow the same process for our production databased hosted in the cloud.
We introduce a small stateful function to get a connection object:
import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
/**
* Initally undefined, but we will use this mutable reference to cache the connection for future use
* Our database contains a single table: 'url'
* A url has two fields: id (Int) and original (String)
*/
let _db;
async function getDB() {
if (_db == null) {
const conn = await open({
filename: './urls.db',
driver: sqlite3.Database,
});
_db = conn;
await _db.run(
'CREATE TABLE IF NOT EXISTS url (id INTEGER PRIMARY KEY AUTOINCREMENT, original TEXT);'
);
}
return _db;
}Users of the database will call getDB() to get a connection and call methods on it. The purpose of _db is simply to cache the connection.
Now we simply need to add a couple helper functions for database access and integrate them into our express app.
/**
* Produces the shortened form of a given URL
* Effect: updates the db to record the url and its shortened id.
*/
async function shortenUrl(url: string): Promise<string> {
const db = await getDB();
const result = await db.run('INSERT INTO url (original) VALUES (?)', url);
console.log(result);
const id = result.lastID;
const short = `http://localhost:3333/s/${id}`;
return short;
}
async function lookupUrl(shortenedId: number) {
const db = await getDB();
const result = await db.get(
'SELECT original FROM url WHERE id = (?)',
shortenedId
);
console.log(result);
return result.original;
}
// App
const app = express();
app.use(express.json());
app.use(cors());
app.post('/api/shorten', async (req, res) => {
const original = req.body.original;
const short = await shortenUrl(original);
res.send({
short: short,
original: original,
});
});
app.get('/s/:id', async (req, res) => {
const id = Number(req.params.id);
const original = await lookupUrl(id);
res.redirect(original);
});Now, go back to your app and try it out. You'll find that even after restarting the backend, previously shortened URLs still work!
Congratulations! You just built a fully functional (and pretty) MVP with persistent storage.
Everything we've done up till now is "exploratory" programming. We didn't care much about the cleanliness of our code or think about how future programmer would interact with it.
In the real world, exploratory programming will get your fired. At Code4Community it means you'll have to redo a lot of work!
Exploratory programming has its place. We used it here for the sake of simplicity and demonstrating how to build a web app in 2-3 files. In this section I'll briefly explain what we would expect from you on a real task.
Whenever you are assigned a task:
- Clarify expected inputs and outputs, whether this is a component, client expectation, or function, you need to understand what you need and what you're producing.
- Understand the purpose of the task. What is the impact? Why is this important?
- Illustrate your understand with examples. What does it "look" like to have this feature completed. How will I show that this works later?
- Performing the task of writing code for a feature means adhering to the good coding guidelines you have already learned in Fundies 1-4.
- Ensure future programmers can understand the code and know when it breaks.
Often times this process is applied to legacy code. Usually defined informally as ugly code with poor documentation and no tests.
That should sound familiar! Let's try to get our frontend and backend in better shape.
Let's start with a minor change. We'll label our important components with an id like below:
<Container maxWidth="4xl" marginBlock={10} textAlign="center">
<Text fontSize="4xl">My URL Shortener</Text>
<form onSubmit={onSubmit}>
<Input
id="url-input"
size="lg"
marginBlock={4}
value={inputUrl}
onChange={(e) => {
setInputUrl(e.target.value);
}}
placeholder="www.my-super-long-url-here.com/12345"
/>
<Button id="submit-btn" type="submit" colorScheme="teal" size="lg">
Generate
</Button>
</form>
<UnorderedList id="url-list" textAlign="left">
{urls.map((u) => (
<ListItem>
<Link href={u.short} color="teal.500">
{u.short}
</Link>{' '}
- {u.original}
</ListItem>
))}
</UnorderedList>
</Container>We're going to do an End to End (E2E) test of our frontend. These tests are usually reserved for core features, emulating real conditions as much as possible in order to be confident that the feature works. They're also cool because you get to see a robot click through your app.
In a single file, we'll describe a programmatic way this robot should interact with our app (in terms of the DOM) and how our app is expected to respond. In the end we'll end up with something like below:
Screen.Recording.2023-06-04.at.10.15.46.AM.mov
Nx has already configured an frontend E2E testing framework called Cypress. Head on over to url/client-e2e/src/e2e/app.cy.ts. Replace the file with the below:
describe('url-client', () => {
beforeEach(() => cy.visit('/'));
it('should generate a short url when a URL is entered', () => {
cy.intercept(
{
method: 'POST', // Route all GET requests
url: 'http://localhost:3333/api/shorten',
},
{
id: 0,
original: 'https://www.c4cneu.com',
short: 'http://my.url/s/0',
}
).as('shorten');
cy.get('#url-input').type('https://www.c4cneu.com');
cy.get('#submit-btn').click();
cy.wait('@shorten');
cy.get('#url-list').should('include.text', '/s/0');
});
});Now we can get to work on changing those components in app.tsx. We can clearly identify two main components (if not more). Let's split them up.
Our plan will be to extract the form into its own component. We should think about how users will use this component, and expose a nice API for them.
Clearly the input field state should be encapsulated into this component, but what should be provided as props to the component?
As a user, I only really care about the original URL that gets submitted when the button is clicked, and I should be able to do whatever I need to (including asynchronous tasks).
So let's have our component take in a single function with the signature (original: string) => Promise<void>
export function App() {
const [urls, setUrls] = useState<Array<Shortened>>([]);
const requestShortUrl = useCallback(
async (inputUrl: string) => {
const response = await axios.post(`http://localhost:3333/api/shorten`, {
original: inputUrl,
});
const newUrl = response.data as Shortened;
setUrls([newUrl, ...urls]);
},
[urls, setUrls]
);
return (
<Container maxWidth="4xl" marginBlock={10} textAlign="center">
<Text fontSize="4xl">My URL Shortener</Text>
<ShortenUrlForm requestShortUrl={requestShortUrl} />
<UnorderedList id="url-list" textAlign="left">
{urls.map((u) => (
<ListItem>
<Link href={u.short} color="teal.500">
{u.short}
</Link>{' '}
- {u.original}
</ListItem>
))}
</UnorderedList>
</Container>
);
}import { Button, Input } from '@chakra-ui/react';
import { FormEvent, useCallback, useState } from 'react';
type ShortenUrlFormProps = {
requestShortUrl: (original: string) => Promise<void>;
};
export const ShortenUrlForm: React.FC<ShortenUrlFormProps> = ({
requestShortUrl,
}) => {
const [inputUrl, setInputUrl] = useState<string>('');
const onSubmit = useCallback(
async (event: FormEvent) => {
event.preventDefault();
await requestShortUrl(inputUrl);
setInputUrl('');
},
[inputUrl, setInputUrl]
);
return (
<form onSubmit={onSubmit}>
<Input
id="url-input"
size="lg"
marginBlock={4}
value={inputUrl}
onChange={(e) => {
setInputUrl(e.target.value);
}}
placeholder="www.my-super-long-url-here.com/12345"
/>
<Button id="submit-btn" type="submit" colorScheme="teal" size="lg">
Generate
</Button>
</form>
);
};
export default ShortenUrlForm;Finally let's talk about component tests, which are similar to E2E tests but generally test individual "stateless" React components. They're faster to run, easier to write, and generally comprise the majority of frontend tests.
In our world, we count "pure"/"stateless" components as those whose behavior is solely determined by their props (modulo some internal useState). This definition doesn't exactly align with the real PL definition but in web dev anything goes...
Let's do this again with the list component, we refactor just like before.
🛑 STOP! Should the list "control" the items in the list as internal state (
useState)? Or take them as props?
Let's move the UnorderedList fragment and type definition of Shortened into their own files:
import { Link, ListItem, UnorderedList } from '@chakra-ui/react';
import { Shortened } from './types';
type UrlListProps = {
urls: Array<Shortened>;
};
export const UrlList: React.FC<UrlListProps> = ({ urls }) => {
return (
<UnorderedList id="urlList" textAlign="left">
{urls.map((u) => (
<ListItem>
<Link href={u.short} color="teal.500">
{u.short}
</Link>{' '}
- {u.original}
</ListItem>
))}
</UnorderedList>
);
};
export default UrlList;Then change app.tsx:
<Container maxWidth="4xl" marginBlock={10} textAlign="center">
<Text fontSize="4xl">My URL Shortener</Text>
<ShortenUrlForm requestShortUrl={requestShortUrl} />
<UrlList urls={urls} />
</Container>Beautiful! Only five lines.
Now we'll learn how to write a component test using Jest and React Testing Library. Unfortunately the Nx template is slight wrong (see https://kentcdodds.com/blog/common-mistakes-with-react-testing-library#not-using-screen)
Delete the contents of app.spec.tsx and use this instead:
import { render, screen } from '@testing-library/react';
import UrlList from './url-list';
import { Shortened } from './types';
describe('UrlList', () => {
it('should render successfully', () => {
const { baseElement } = render(<UrlList urls={[]} />);
expect(baseElement).toBeTruthy();
});
it('should contain the list of URLs provided', () => {
const urls: Array<Shortened> = [
{ original: 'https://c4cneu.com', short: 'http://short.com/s/0' },
];
render(<UrlList urls={urls} />);
expect(screen.getByText(urls[0].original, { exact: false })).toBeTruthy();
expect(screen.getByText(urls[0].short, { exact: false })).toBeTruthy();
});
});This should look more or less the same as the Cypress test, but without API mocking. We need to pass in some props, and assert the effects of the component via interactions with the DOM.
Run the test with nx test url-client. Compare the time it takes to run versus nx e2e url-client-e2e. You should see a considerable difference. This is mainly due to the fact the Cypress needs to spin up a headless browser, while Jest can use a mock virtual DOM.
Once again, the backend will be more straightforward as it aligns closely to the kind of programming you've seen in classes. If you're done Fundies 2 this should be a breeze.
When refactoring for testability, our primary concern should be how to seperate pure and impure code. The more we can fragment our code into purely functional parts, the easier it becomes to test.
🛑 STOP! What impure effect is "polluting" the rest of the code in
main.ts?
One of the core flaws in this design is the lack of seperation between the core logic of the backend and dispatches to storage. We should codify this through an API.
Considering how we've already split up main.ts, we can simply move the SQL code and actions into their own file and export the actions.
import express from 'express';
import cors from 'cors';
import { shortenUrl, lookupUrl } from './persist';
...
// App
const app = express();
app.use(express.json());
app.use(cors());Then, let's introduce some dependency injection, which is just a fancy term for supplying effectful dependencies at runtime, so they can be easily provided and switched out for fakes/mocks/stubs.
import express from 'express';
import cors from 'cors';
import { shortenUrl, lookupUrl } from './persist';
/**
* Stateful dependencies to inject at root.
*/
type MainDependencies = {
shortenUrl: (original: string) => Promise<string>;
lookupUrl: (shortId: number) => Promise<string>;
};
// Composition Root
const deps: MainDependencies = {
shortenUrl,
lookupUrl,
};
// App
export async function main({ shortenUrl, lookupUrl }: MainDependencies) {
const app = express();
app.use(express.json());
app.use(cors());
app.post('/api/shorten', async (req, res) => {
const original = req.body.original;
const short = await shortenUrl(original);
res.send({
short: short,
original: original,
});
});
app.get('/s/:id', async (req, res) => {
const id = Number(req.params.id);
const original = await lookupUrl(id);
res.redirect(original);
});
const port = process.env.PORT || 3333;
const server = app.listen(port, () => {
console.log(`Listening at http://localhost:${port}/api`);
});
server.on('error', console.error);
}
main(deps);Now, we'll seperate the Express app itself from the main script which starts the server. This will allow us to observe and interact with the app independently, which is essential for testing. Let's name our new app file app.ts.
import { shortenUrl, lookupUrl } from './persist';
import { createApp } from './app';
// Composition Root
async function main() {
const app = await createApp({
shortenUrl,
lookupUrl,
});
const port = process.env.PORT || 3333;
const server = app.listen(port, () => {
console.log(`Listening at http://localhost:${port}/api`);
});
server.on('error', console.error);
}
main();import express from 'express';
import cors from 'cors';
/**
* * Stateful dependencies to inject at root.
*/
type MainDependencies = {
shortenUrl: (original: string) => Promise<string>;
lookupUrl: (shortId: number) => Promise<string>;
};
export async function createApp({ shortenUrl, lookupUrl }: MainDependencies) {
const app = express();
app.use(express.json());
app.use(cors());
app.post('/api/shorten', async (req, res) => {
const original = req.body.original;
const short = await shortenUrl(original);
res.status(200).send({
short: short,
original: original,
});
});
app.get('/s/:id', async (req, res) => {
const id = Number(req.params.id);
const original = await lookupUrl(id);
res.redirect(original);
});
return app;
}Finally we're ready to test our API properly.
Remember when we used Postman? Well Postman is excellent for exploring an API, and interacting with a live production version. However, Postman is not a replacement for automated unit tests!
Let's learn a principled approach to API testing using supertest. Install it from the root directory with:
yarn add -D supertestAnd skim the usage here: https://www.npmjs.com/package/supertest
Then, add a new test file app.spec.ts.
import request from 'supertest';
import { createApp } from './app';
describe('App', () => {
let app;
let urls: Array<string>;
beforeEach(async () => {
urls = [];
const shortenUrl = async (original: string) => {
urls = [...urls, original];
return `http://localhost:3333/${urls.length}`;
};
const lookupUrl = async (shortId: number) => {
return urls[shortId];
};
app = await createApp({
shortenUrl,
lookupUrl,
});
});
it('should store shortened urls', async () => {
const original = 'www.example.com/123';
console.log(app);
const response = await request(app)
.post('/api/shorten')
.send({ original: original });
expect(response.status).toEqual(201);
expect(urls).toContain(original);
});
});Run this with nx test url-server.
It looks like the test fails! Check the console to find out why the test is failing and fix it.
Hint: POST requests are supposed to respond with status code 201. Does our POST endpoint do this?
We've managed to test our API by mocking the unreliable effects with fast in-memory alternatives, and using supertest to emulate HTTP requests to the Express router without ever actually starting the server!
If you've completed everything so far as instructed, you've shown that you can:
- Create a client side rendered web application
- Create a RESTful API in Express
- Write component tests for the React app
- Write E2E tests using Cypress
- Write unit tests for the Expresss router
- Style React components using ChakraUI
- Interact with REST APIs with Postman
- Persist data to a database
Not covered was:
- Authentication and Authorization
- Global state (Redux, Providers, Zustand) and query caching (React Query, SWR)
- Frontend error/loading/success patterns and conditional rendering
- Working with cloud technologies
- Using secrets and environment variables
- Documenting changes in code and in a README
To submit this challenge:
List at least three things you would do to improve the quality of the codebase in order of importance.
Implement some kind of improvement to what you have so far. This could be as small as a README or as significant as rewriting a component in the language/framework of your choice. Here are some ideas to get you started:
- The ideas above
- Use a different kind of persistent storage
- Host this web app
- Add the ability to encode URLs as QR codes
- Diagram the components and interactions
- Add proper input validation and output sanitization where needed