diff --git a/client/package.json b/client/package.json
index 1ab2a74517def44f50cc688355c9dfa64cd7ccfe..0a4ff64eed3b791fc2787cc1298eba4fb6c896fc 100644
--- a/client/package.json
+++ b/client/package.json
@@ -15,7 +15,6 @@
         "@types/react": "^16.9.53",
         "@types/react-dom": "^16.9.8",
         "@types/react-router-dom": "^5.1.7",
-        "axios": "^0.21.1",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
         "react-router-dom": "^5.2.0",
diff --git a/client/src/adapters/api.ts b/client/src/adapters/api.ts
deleted file mode 100644
index ae5d6a5b986b51d1e01158071631b75e51e80764..0000000000000000000000000000000000000000
--- a/client/src/adapters/api.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import axios from 'axios';
-
-const baseUrl = 'http://localhost:8000/v1';
-
-export const authAxios = axios.create({
-    baseURL: baseUrl,
-    headers: {
-        Authorization: `Bearer ${getAccessToken}`
-    }
-});
-
-export function isLoggedIn() {
-    return getAccessToken();
-}
-
-export function getAccessToken(): string | null {
-    return localStorage.getItem('access-token');
-}
-
-export function setAccesstoken(token: string): void {
-    localStorage.setItem('access-token', token);
-}
-
-export function registerUser(username: string, password: string) {
-    return new Promise(function (resolve, reject) {
-        axios.post(`${baseUrl}/auth/register`, {
-            username: username,
-            password: password
-        }).then(() => {
-            axios.post(`${baseUrl}/auth/token`, {
-                username: username,
-                password: password
-            }).then(({ data }) => {
-                setAccesstoken(data.token);
-                resolve(true);
-            }).catch((e) => {
-                reject(e);
-            });
-        }).catch((e) => {
-            reject(e);
-        });
-    })
-}
diff --git a/client/src/adapters/auth.ts b/client/src/adapters/auth.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d96373339b0407d006e0ba37dc0e4f4cff63e6dd
--- /dev/null
+++ b/client/src/adapters/auth.ts
@@ -0,0 +1,89 @@
+
+import { apiRoot } from 'config';
+
+export function getAuthHeader(): HeadersInit {
+    if (isLoggedIn()) {
+        return {
+            'Authorization': `Brearer ${getToken()}`,
+        };
+    } else {
+        return {};
+    }
+}
+
+export function isLoggedIn(): boolean {
+    return typeof getToken() === 'string';
+}
+
+export function getToken(): string | null {
+    return localStorage.getItem('access-token');
+}
+
+export function setToken(token: string) {
+    localStorage.setItem('access-token', token);
+}
+
+export function clearToken() {
+    localStorage.removeItem('access-token');
+}
+
+setInterval(extendAccessToken, 1000 * 60 * 30);
+
+async function extendAccessToken() {
+    if (isLoggedIn()) {
+        const response = await fetch(`${apiRoot}/auth/extend`, { headers: getAuthHeader() });
+        if (response.ok) {
+            const json = await response.json();
+            setToken(json.token);
+        } else if (response.status === 403) {
+            clearToken();
+        }
+    }
+}
+
+export async function register(username: string, password: string): Promise<boolean> {
+    try {
+        const response = await fetch(`${apiRoot}/auth/register`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+                username: username,
+                password: password,
+            }),
+        });
+        if (response.ok) {
+            const json = await response.json();
+            setToken(json.token);
+        }
+        return response.ok;
+    } catch (e) {
+        // Probably a network error
+        throw e;
+    }
+}
+
+export async function login(username: string, password: string): Promise<boolean> {
+    try {
+        const response = await fetch(`${apiRoot}/auth/token`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json'
+            },
+            body: JSON.stringify({
+                username: username,
+                password: password
+            }),
+        });
+        if (response.ok) {
+            const json = await response.json();
+            setToken(json.token);
+        }
+        return response.ok;
+    } catch (e) {
+        // Probably a network error
+        throw e;
+    }
+}
+
diff --git a/client/src/adapters/constants.ts b/client/src/adapters/constants.ts
deleted file mode 100644
index c5c18b169fcc2ade194ac898739505c92a1fdff9..0000000000000000000000000000000000000000
--- a/client/src/adapters/constants.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-
-export const DEMO_CONSTANT = 42;
-
diff --git a/client/src/adapters/user.ts b/client/src/adapters/user.ts
new file mode 100644
index 0000000000000000000000000000000000000000..655b475eaed9c3493c7d661b9bcfe417abda9b1d
--- /dev/null
+++ b/client/src/adapters/user.ts
@@ -0,0 +1,13 @@
+
+import { apiRoot } from 'config';
+
+export async function exists(username: string) {
+    try {
+        const response = await fetch(`${apiRoot}/user/name/${username}`);
+        return response.ok;
+    } catch (e) {
+        // Probably a network error
+        return false;
+    }
+}
+
diff --git a/client/src/components/forms/RegisterForm/index.tsx b/client/src/components/forms/RegisterForm/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0d8b6462fb5368ab59e52b420e325ea00ca3b5f4
--- /dev/null
+++ b/client/src/components/forms/RegisterForm/index.tsx
@@ -0,0 +1,66 @@
+
+import { FormEvent, useCallback, useState } from 'react';
+
+import TextInput from 'components/ui/TextInput';
+import Button from 'components/ui/Button';
+import { exists } from 'adapters/user';
+
+import './register-form.scss';
+
+async function validateUsername(username: string) {
+    if (username?.length < 3) {
+        return 'Username has to be at least 4 characters long.';
+    } else if (await exists(username)) {
+        return 'Username is already taken by someone else.';
+    } else {
+        return null;
+    }
+}
+
+function validatePassword(password: string) {
+    if (password?.length < 3) {
+        return 'Password has to be at least 6 characters long';
+    } else {
+        return null;
+    }
+}
+
+interface Props {
+    onSubmit?: (username: string, password: string) => void
+}
+
+export default function RegisterForm({ onSubmit }: Props) {
+    const [username, setUsername] = useState<string>('');
+    const [password, setPassword] = useState<string>('');
+
+    const handleSubmit = useCallback(async (e: FormEvent) => {
+        e.preventDefault();
+        if (await validateUsername(username) === null && validatePassword(password) === null) {
+            onSubmit?.(username, password);
+        }
+    }, [ onSubmit, password, username ]);
+
+    return (
+        <form onSubmit={handleSubmit}>
+            <TextInput
+                label="Username"
+                name="username"
+                color="dark"
+                onChange={setUsername}
+                validation={validateUsername}
+            />
+            <TextInput
+                label="Password"
+                name="password"
+                color="dark"
+                type="password"
+                onChange={setPassword}
+                validation={validatePassword}
+            />
+            <Button type="submit">
+                Register now
+            </Button>
+        </form>
+    );
+}
+
diff --git a/client/src/components/forms/RegisterForm/register-form.scss b/client/src/components/forms/RegisterForm/register-form.scss
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/client/src/components/forms/RegisterForm/register-form.scss
@@ -0,0 +1 @@
+
diff --git a/client/src/components/helpers/LoginRoute.tsx b/client/src/components/helpers/LoginRoute.tsx
index 3019b52336377bbcda85cb7e071534e50c8ca300..47f8be758d41d889732782c6573180609ea96128 100644
--- a/client/src/components/helpers/LoginRoute.tsx
+++ b/client/src/components/helpers/LoginRoute.tsx
@@ -1,6 +1,6 @@
 
 import { Route, RouteProps, useHistory } from 'react-router-dom';
-import { isLoggedIn } from 'adapters/api';
+import { isLoggedIn } from 'adapters/auth';
 import { useEffect } from 'react';
 
 export default function LoginRoute(props: RouteProps) {
diff --git a/client/src/components/helpers/ProtectedRoute.tsx b/client/src/components/helpers/ProtectedRoute.tsx
index fae296c8396711b7bac8729b13949f45751ece87..136b7e6b1e9c5730bd34a545450aa10480042845 100644
--- a/client/src/components/helpers/ProtectedRoute.tsx
+++ b/client/src/components/helpers/ProtectedRoute.tsx
@@ -1,6 +1,6 @@
 
 import { Route, RouteProps, useHistory } from 'react-router-dom';
-import { isLoggedIn } from 'adapters/api';
+import { isLoggedIn } from 'adapters/auth';
 import { useEffect } from 'react';
 
 export default function ProtectedRoute(props: RouteProps) {
diff --git a/client/src/components/ui/Page/index.tsx b/client/src/components/ui/Page/index.tsx
index e1fcb5e12159b69850a3accb94306f71abd6ee66..07514fb2ca897b88efed5906e2ca06485c0ca786 100644
--- a/client/src/components/ui/Page/index.tsx
+++ b/client/src/components/ui/Page/index.tsx
@@ -1,4 +1,6 @@
+
 import { ReactNode } from 'react';
+
 import Header from 'components/ui/Header';
 import './page.scss';
 
@@ -18,4 +20,5 @@ export default function Page({ children, className, header }: Props) {
             </main>
         </>
     );
-}
\ No newline at end of file
+}
+
diff --git a/client/src/components/ui/Page/page.scss b/client/src/components/ui/Page/page.scss
index f4fddeb15a496e3e50a00cd8b5197ebe44121d57..2a75edab28643e8e9290ad7b5b9b4e8d70b6b7cd 100644
--- a/client/src/components/ui/Page/page.scss
+++ b/client/src/components/ui/Page/page.scss
@@ -1,3 +1,4 @@
+
 .page-container {
     max-width: 1440px;
     min-height: 100vh;
@@ -5,3 +6,4 @@
     background: rgba(255, 255, 255, 0.4);
     backdrop-filter: blur(30px);
 }
+
diff --git a/client/src/components/ui/TextInput/index.tsx b/client/src/components/ui/TextInput/index.tsx
index af6519929dc7379094fa87c421361ffbe51f7053..cfc0351fbcefc3ac74df06b63d5f3ea2a0ff9d57 100644
--- a/client/src/components/ui/TextInput/index.tsx
+++ b/client/src/components/ui/TextInput/index.tsx
@@ -1,4 +1,5 @@
-import React, { ChangeEvent, Dispatch, FocusEvent, useState } from "react";
+
+import { ChangeEvent, Dispatch, FocusEvent, useCallback, useState } from "react";
 
 import './text-input.scss';
 
@@ -8,30 +9,20 @@ interface Props {
     color?: 'dark'
     type?: 'password' | 'textarea' | 'text',
     onChange: Dispatch<string>,
-    validateFn: (arg0: string) => string | null;
+    validation: (text: string) => Promise<string | null> | string | null;
 }
 
-export default function TextInput({ label, name, type, color, onChange, validateFn }: Props) {
+export default function TextInput({ label, name, type, color, onChange, validation }: Props) {
     const [error, setError] = useState('');
 
-    type = type ?? 'text';
-
-    const setValue = (e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>): void => {
+    const handleChange = useCallback((e: ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
         onChange(e.target.value);
-    }
-
-    const validateField = (e: FocusEvent<HTMLTextAreaElement | HTMLInputElement>): void => {
-        if (validateFn) {
-            let error = validateFn(e.target.value);
-            
-            if (error)
-                setError(error);
-            else
-                setError('');
-        }
-    }
+    }, [ onChange ]);
 
-    const errorTag = error ? (<div className="error">{error}</div>) : null;
+    const handleBlur = useCallback(async (e: FocusEvent<HTMLTextAreaElement | HTMLInputElement>) => {
+        let error = await validation?.(e.target.value);
+        setError(error ?? '');
+    }, [ validation ]);
 
     return (
         <div className={'input-element' + (type === 'textarea' ? ' textarea' : '')}>
@@ -39,11 +30,11 @@ export default function TextInput({ label, name, type, color, onChange, validate
                 <label htmlFor={name}>{label}</label>
                 {
                     type === 'textarea' ?
-                        (<textarea onChange={setValue} name={name} id={name} onBlur={validateField}></textarea>)
-                        : (<input onChange={setValue} type={type} name={name} id={name} onBlur={validateField} />)
+                        (<textarea onChange={handleChange} name={name} id={name} onBlur={handleBlur} />)
+                        : (<input onChange={handleChange} type={type} name={name} id={name} onBlur={handleBlur} autoComplete="off"/>)
                 }
             </div >
-            {errorTag}
+            <div className="error">{error}</div>
         </div>
     );
 }
diff --git a/client/src/components/ui/TextInput/text-input.scss b/client/src/components/ui/TextInput/text-input.scss
index 772b9c27aaa97bd58018eb353fa422ccd145c8c5..d14feaa3c4eeee4ae46f9d9e4c623617a36dd70c 100644
--- a/client/src/components/ui/TextInput/text-input.scss
+++ b/client/src/components/ui/TextInput/text-input.scss
@@ -1,3 +1,4 @@
+
 @use 'styles/settings.scss'as s;
 @use 'styles/mixins.scss'as mx;
 
@@ -8,6 +9,7 @@
         color: s.$error-color;
         margin-top: 10px;
         padding-left: 15px;
+        height: 1rem;
     }
 
     .input-field {
@@ -16,7 +18,6 @@
         width: 100%;
 
         &.dark {
-
             textarea,
             input {
                 color: s.$body-color;
@@ -24,10 +25,8 @@
             }
 
             &:before {
-
                 background: rgba(0, 0, 0, 0.05);
             }
-
         }
 
         &:before {
@@ -49,7 +48,6 @@
             font-weight: s.$weight-bold;
             z-index: 20;
             transform: translateY(-50%);
-
         }
 
         textarea {
@@ -74,4 +72,5 @@
             background: rgba(255, 255, 255, 0.1);
         }
     }
-}
\ No newline at end of file
+}
+
diff --git a/client/src/config.ts b/client/src/config.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cd15358f0b3a31f36a7a5d1dc4d395a6af9aec53
--- /dev/null
+++ b/client/src/config.ts
@@ -0,0 +1,3 @@
+
+export const apiRoot = 'http://localhost:8000/v1';
+
diff --git a/client/src/pages/Register/index.tsx b/client/src/pages/Register/index.tsx
index 08781570f5653144d676dc76b89da477c085fddf..6dc9aaa4deddac5c078d23f5d96f8d8438f2b453 100644
--- a/client/src/pages/Register/index.tsx
+++ b/client/src/pages/Register/index.tsx
@@ -1,64 +1,32 @@
-import Page from 'components/ui/Page';
-import TextInput from 'components/ui/TextInput';
-import Button from 'components/ui/Button';
-import { FormEvent, useState } from 'react';
+
+import { useCallback } from 'react';
 import { Link, useHistory } from 'react-router-dom';
-import { registerUser } from 'adapters/api';
-import './register.scss';
 
-function usernameIsValid(username: string) {
-    return (username && username.length > 3) ? null : 'Username has to be at least 4 characters long.';
-}
+import Page from 'components/ui/Page';
+import RegisterForm from 'components/forms/RegisterForm';
+import { register } from 'adapters/auth';
 
-function passwordIsValid(password: string) {
-    return (password && password.length > 5) ? null : 'Password has to be at least 6 characters long';
-}
+import './register.scss';
 
 export default function Register() {
-    const [username, setUsername] = useState<string>('');
-    const [password, setPassword] = useState<string>('');
-
     const history = useHistory();
 
-
-    const register = async (e: FormEvent) => {
-        e.preventDefault();
-
-        if (usernameIsValid(username) && passwordIsValid(password)) {
-            await registerUser(username, password).then((data) => {
+    const handleSubmit = useCallback(async (username: string, password: string) => {
+        try {
+            if (await register(username, password)) {
                 history.push('/tasks');
-            }).catch(() => {
-            });
-
-        }
-    }
+            }
+        } catch (e) { }
+    }, [ history ]);
 
     return (
         <Page header={false}>
             <div className="content-container">
                 <h1>Register</h1>
-                <form onSubmit={register}>
-                    <TextInput
-                        label="Username"
-                        name="username"
-                        color="dark"
-                        onChange={setUsername}
-                        validateFn={usernameIsValid}
-                    />
-                    <TextInput
-                        label="Password"
-                        name="password"
-                        color="dark"
-                        type="password"
-                        onChange={setPassword}
-                        validateFn={passwordIsValid}
-                    />
-                    <Button type="submit">
-                        Register now
-                    </Button>
-                </form>
+                <RegisterForm onSubmit={handleSubmit} />
                 <Link to="/login">You already have an account?</Link>
             </div>
         </Page>
     );
-}
\ No newline at end of file
+}
+
diff --git a/client/src/service-worker.ts b/client/src/service-worker.ts
index b714b8bc0432513cdff132e11f800e9170518688..2596bac005e729afb695c692b400c91e50812ea3 100644
--- a/client/src/service-worker.ts
+++ b/client/src/service-worker.ts
@@ -1,11 +1,11 @@
 /// <reference lib="webworker" />
 /* eslint-disable no-restricted-globals */
 
-import {clientsClaim} from 'workbox-core';
-import {ExpirationPlugin} from 'workbox-expiration';
-import {precacheAndRoute, createHandlerBoundToURL} from 'workbox-precaching';
-import {registerRoute} from 'workbox-routing';
-import {StaleWhileRevalidate} from 'workbox-strategies';
+import { clientsClaim } from 'workbox-core';
+import { ExpirationPlugin } from 'workbox-expiration';
+import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
+import { registerRoute } from 'workbox-routing';
+import { StaleWhileRevalidate } from 'workbox-strategies';
 
 declare const self: ServiceWorkerGlobalScope;
 
@@ -45,11 +45,10 @@ registerRoute(
     createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
 );
 
-// An example runtime caching route for requests that aren't handled by the
-// precache, in this case same-origin .png requests like those from in public/
+// Runtime caching route for images requests that aren't handled by the precache.
 registerRoute(
     // Add in any other file extensions or routing criteria as needed.
-    ({url}) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
+    ({url}) => url.origin === self.location.origin && url.pathname.match(/.(png|jpg|jpeg|gif)$/),
     // Customize this strategy as needed, e.g., by changing to CacheFirst.
     new StaleWhileRevalidate({
         cacheName: 'images',
diff --git a/client/yarn.lock b/client/yarn.lock
index f85d7d33f624100f060ec29e76a647e5f27a4cd1..288d64d8d6572266cdd78a3c23b411594d5b5f04 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -2580,13 +2580,6 @@ axe-core@^4.0.2:
   resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.4.tgz#f19cd99a84ee32a318b9c5b5bb8ed373ad94f143"
   integrity sha512-Pdgfv6iP0gNx9ejRGa3zE7Xgkj/iclXqLfe7BnatdZz0QnLZ3jrRHUVH8wNSdN68w05Sk3ShGTb3ydktMTooig==
 
-axios@^0.21.1:
-  version "0.21.1"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
-  integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
-  dependencies:
-    follow-redirects "^1.10.0"
-
 axobject-query@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -5094,7 +5087,7 @@ flush-write-stream@^1.0.0:
     inherits "^2.0.3"
     readable-stream "^2.3.6"
 
-follow-redirects@^1.0.0, follow-redirects@^1.10.0:
+follow-redirects@^1.0.0:
   version "1.13.3"
   resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
   integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==