diff --git a/maven-settings.xml b/maven-settings.xml
index adce7ab..18a15cb 100644
--- a/maven-settings.xml
+++ b/maven-settings.xml
@@ -1,12 +1,12 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<settings xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"
-		  xmlns="http://maven.apache.org/SETTINGS/1.1.0"
-		  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
-	<servers>
-		<server>
-			<id>repo.gittr.ch</id>
-			<username>ci</username>
-			<password>${env.REPO_TOKEN}</password>
-		</server>
-	</servers>
+<settings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"
+          xmlns="http://maven.apache.org/SETTINGS/1.1.0">
+    <servers>
+        <server>
+            <id>repo.gittr.ch</id>
+            <username>ci</username>
+            <password>${env.REPO_TOKEN}</password>
+        </server>
+    </servers>
 </settings>
diff --git a/pom.xml b/pom.xml
index 5035e90..9b3e6c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,126 +1,181 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-	<modelVersion>4.0.0</modelVersion>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
 
-	<parent>
-		<groupId>ch.fritteli</groupId>
-		<artifactId>fritteli-build-parent</artifactId>
-		<version>2.0.4</version>
-	</parent>
+    <parent>
+        <groupId>ch.fritteli</groupId>
+        <artifactId>fritteli-build-parent</artifactId>
+        <version>2.0.4</version>
+    </parent>
 
-	<groupId>ch.fritteli.labyrinth</groupId>
-	<artifactId>labyrinth-server</artifactId>
-	<version>0.0.2-SNAPSHOT</version>
+    <groupId>ch.fritteli.labyrinth</groupId>
+    <artifactId>labyrinth-server</artifactId>
+    <version>0.0.2-SNAPSHOT</version>
+    <description>The Labyrinth server, offering a REST endpoint to access the Labyrinth Generator.</description>
+    <url>https://manuel.friedli.info/labyrinth.html</url>
 
-	<properties>
-		<logback.version>1.4.6</logback.version>
-		<lombok.version>1.18.26</lombok.version>
-		<slf4j.version>2.0.5</slf4j.version>
-		<java.source.version>17</java.source.version>
-		<java.target.version>17</java.target.version>
-	</properties>
+    <properties>
+        <java.source.version>17</java.source.version>
+        <java.target.version>17</java.target.version>
+        <jetbrains-annotations.version>24.0.1</jetbrains-annotations.version>
+        <junit-jupiter.version>5.9.2</junit-jupiter.version>
+        <labyrinth-generator.version>0.0.4</labyrinth-generator.version>
+        <logback.version>1.4.6</logback.version>
+        <lombok.version>1.18.26</lombok.version>
+        <slf4j.version>2.0.7</slf4j.version>
+        <undertow.version>2.3.5.Final</undertow.version>
+        <vavr.version>0.10.4</vavr.version>
+    </properties>
 
-	<dependencies>
-		<dependency>
-			<groupId>ch.fritteli.labyrinth</groupId>
-			<artifactId>labyrinth-generator</artifactId>
-			<version>0.0.2</version>
-		</dependency>
-		<dependency>
-			<groupId>io.vavr</groupId>
-			<artifactId>vavr</artifactId>
-		</dependency>
-		<dependency>
-			<groupId>org.projectlombok</groupId>
-			<artifactId>lombok</artifactId>
-		</dependency>
-		<dependency>
-			<groupId>org.jetbrains</groupId>
-			<artifactId>annotations</artifactId>
-		</dependency>
-		<dependency>
-			<groupId>org.slf4j</groupId>
-			<artifactId>slf4j-api</artifactId>
-			<version>${slf4j.version}</version>
-		</dependency>
-		<dependency>
-			<groupId>ch.qos.logback</groupId>
-			<artifactId>logback-classic</artifactId>
-			<version>${logback.version}</version>
-		</dependency>
-		<dependency>
-			<groupId>org.junit.jupiter</groupId>
-			<artifactId>junit-jupiter-api</artifactId>
-			<scope>test</scope>
-		</dependency>
-	</dependencies>
+    <developers>
+        <developer>
+            <name>Manuel Friedli</name>
+            <id>manuel</id>
+            <url>https://www.fritteli.ch/</url>
+            <email>manuel@fritteli.ch</email>
+            <timezone>Europe/Zurich</timezone>
+            <roles>
+                <role>Project Lead</role>
+                <role>Software Architect</role>
+                <role>Software Engineer</role>
+                <role>Operations Manager</role>
+            </roles>
+        </developer>
+    </developers>
 
-	<build>
-		<plugins>
-			<plugin>
-				<groupId>org.apache.maven.plugins</groupId>
-				<artifactId>maven-shade-plugin</artifactId>
-				<version>3.2.4</version>
-				<configuration>
-					<transformers>
-						<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
-							<mainClass>ch.fritteli.labyrinth.server.Main</mainClass>
-						</transformer>
-					</transformers>
-				</configuration>
-				<executions>
-					<execution>
-						<phase>package</phase>
-						<goals>
-							<goal>shade</goal>
-						</goals>
-					</execution>
-				</executions>
-			</plugin>
-		</plugins>
-	</build>
-	<scm>
-		<connection>scm:git:git://gittr.ch/java/labyrinth-server.git</connection>
-		<developerConnection>scm:git:ssh://git@gittr.ch/java/labyrinth-server.git</developerConnection>
-		<url>https://gittr.ch/java/labyrinth-server</url>
-		<tag>v0.0.1</tag>
-	</scm>
-	<distributionManagement>
-		<repository>
-			<id>repo.gittr.ch</id>
-			<name>gittr.ch</name>
-			<url>https://repo.gittr.ch/releases/</url>
-		</repository>
-		<snapshotRepository>
-			<id>repo.gittr.ch</id>
-			<name>gittr.ch</name>
-			<url>https://repo.gittr.ch/snapshots/</url>
-		</snapshotRepository>
-	</distributionManagement>
-	<repositories>
-		<repository>
-			<id>repo.gittr.ch.releases</id>
-			<url>https://repo.gittr.ch/releases/</url>
-			<releases>
-				<enabled>true</enabled>
-				<updatePolicy>never</updatePolicy>
-			</releases>
-			<snapshots>
-				<enabled>false</enabled>
-				<updatePolicy>never</updatePolicy>
-			</snapshots>
-		</repository>
-		<repository>
-			<id>repo.gittr.ch.snapshots</id>
-			<url>https://repo.gittr.ch/snapshots/</url>
-			<releases>
-				<enabled>false</enabled>
-				<updatePolicy>never</updatePolicy>
-			</releases>
-			<snapshots>
-				<enabled>true</enabled>
-				<updatePolicy>always</updatePolicy>
-			</snapshots>
-		</repository>
-	</repositories>
+    <dependencies>
+        <dependency>
+            <groupId>ch.fritteli.labyrinth</groupId>
+            <artifactId>labyrinth-generator</artifactId>
+            <version>${labyrinth-generator.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.vavr</groupId>
+            <artifactId>vavr</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>${logback.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.undertow</groupId>
+            <artifactId>undertow-core</artifactId>
+            <version>${undertow.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>3.4.1</version>
+                <configuration>
+                    <transformers>
+                        <transformer
+                                implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                            <mainClass>ch.fritteli.labyrinth.server.Main</mainClass>
+                        </transformer>
+                    </transformers>
+                    <filters>
+                        <filter>
+                            <artifact>ch.fritteli.labyrinth:labyrinth-generator</artifact>
+                            <excludes>
+                                <exclude>logback.xml</exclude>
+                            </excludes>
+                        </filter>
+                    </filters>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>attach-sources</id>
+                        <phase>verify</phase>
+                        <goals>
+                            <goal>jar-no-fork</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-site-plugin</artifactId>
+                <version>4.0.0-M6</version>
+            </plugin>
+        </plugins>
+    </build>
+    <scm>
+        <connection>scm:git:https://gittr.ch/java/labyrinth-server.git</connection>
+        <developerConnection>scm:git:ssh://git@gittr.ch/java/labyrinth-server.git</developerConnection>
+        <url>https://gittr.ch/java/labyrinth-server</url>
+        <tag>HEAD</tag>
+    </scm>
+    <distributionManagement>
+        <repository>
+            <id>repo.gittr.ch</id>
+            <name>gittr.ch</name>
+            <url>https://repo.gittr.ch/releases/</url>
+        </repository>
+        <snapshotRepository>
+            <id>repo.gittr.ch</id>
+            <name>gittr.ch</name>
+            <url>https://repo.gittr.ch/snapshots/</url>
+        </snapshotRepository>
+    </distributionManagement>
+    <repositories>
+        <repository>
+            <id>repo.gittr.ch.releases</id>
+            <url>https://repo.gittr.ch/releases/</url>
+            <releases>
+                <enabled>true</enabled>
+                <updatePolicy>never</updatePolicy>
+            </releases>
+            <snapshots>
+                <enabled>false</enabled>
+                <updatePolicy>never</updatePolicy>
+            </snapshots>
+        </repository>
+        <repository>
+            <id>repo.gittr.ch.snapshots</id>
+            <url>https://repo.gittr.ch/snapshots/</url>
+            <releases>
+                <enabled>false</enabled>
+                <updatePolicy>never</updatePolicy>
+            </releases>
+            <snapshots>
+                <enabled>true</enabled>
+                <updatePolicy>always</updatePolicy>
+            </snapshots>
+        </repository>
+    </repositories>
 </project>
diff --git a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java
index aab20f6..16e0aae 100644
--- a/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java
+++ b/src/main/java/ch/fritteli/labyrinth/server/LabyrinthServer.java
@@ -1,323 +1,59 @@
 package ch.fritteli.labyrinth.server;
 
-import ch.fritteli.labyrinth.generator.model.Labyrinth;
-import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer;
-import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer;
-import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer;
-import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer;
-import com.sun.net.httpserver.Headers;
-import com.sun.net.httpserver.HttpExchange;
-import com.sun.net.httpserver.HttpServer;
-import io.vavr.collection.HashMap;
-import io.vavr.collection.HashSet;
-import io.vavr.collection.List;
-import io.vavr.collection.Map;
-import io.vavr.collection.Set;
-import io.vavr.collection.Stream;
-import io.vavr.control.Option;
+import ch.fritteli.labyrinth.server.handler.CreateHandler;
+import ch.fritteli.labyrinth.server.handler.RenderHandler;
+import io.undertow.Undertow;
+import io.undertow.server.RoutingHandler;
 import io.vavr.control.Try;
-import lombok.Getter;
+import java.net.InetSocketAddress;
 import lombok.NonNull;
 import lombok.extern.slf4j.Slf4j;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.InetSocketAddress;
-import java.nio.charset.StandardCharsets;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.SynchronousQueue;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Function;
 
 @Slf4j
 public class LabyrinthServer {
-    @NonNull
-    private final HttpServer httpServer;
-    @NonNull
-    private final ExecutorService executorService = new ThreadPoolExecutor(0,
-                                                                           1_000,
-                                                                           5,
-                                                                           TimeUnit.SECONDS,
-                                                                           new SynchronousQueue<>()
-    );
 
-    public static Option<LabyrinthServer> createAndStartServer() {
-        final Option<LabyrinthServer> serverOption = Try.of(ServerConfig::init)
-                                                        .mapTry(LabyrinthServer::new)
-                                                        .onFailure(cause -> log.error(
-                                                                "Failed to create LabyrinthServer.",
-                                                                cause
-                                                        ))
-                                                        .toOption();
-        serverOption.forEach(LabyrinthServer::start);
-        return serverOption;
+    @NonNull
+    private final Undertow undertow;
+
+    private LabyrinthServer(@NonNull final ServerConfig config) {
+        final String hostAddress = config.getAddress().getHostAddress();
+        final int port = config.getPort();
+        log.info("Starting Server at http://{}:{}/", hostAddress, port);
+        final RoutingHandler routingHandler = new RoutingHandler()
+                .get(CreateHandler.PATH_TEMPLATE, new CreateHandler())
+                .post(RenderHandler.PATH_TEMPLATE, new RenderHandler());
+
+        this.undertow = Undertow.builder()
+                .addHttpListener(port, hostAddress)
+                .setHandler(routingHandler)
+                .build();
     }
 
-    public LabyrinthServer(@NonNull final ServerConfig config) throws IOException {
-        this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5);
-        this.httpServer.createContext("/", new StaticResourcesFileHandler(this.executorService));
-        this.httpServer.createContext("/create", this::handleCreate);
-        this.httpServer.createContext("/render", this::handleRender);
+    @NonNull
+    public static Try<LabyrinthServer> createAndStartServer() {
+        return Try.of(ServerConfig::init)
+                .flatMapTry(LabyrinthServer::createAndStartServer);
     }
 
-    public void start() {
+    @NonNull
+    public static Try<LabyrinthServer> createAndStartServer(@NonNull final ServerConfig config) {
+        return Try.of(() -> new LabyrinthServer(config))
+                .peek(LabyrinthServer::start);
+    }
+
+    private void start() {
         Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper"));
-        this.httpServer.start();
-        log.info("Listening on http://{}:{}",
-                 this.httpServer.getAddress().getHostString(),
-                 this.httpServer.getAddress().getPort()
-        );
+        this.undertow.start();
+        final InetSocketAddress address = (InetSocketAddress) this.undertow.getListenerInfo().get(0).getAddress();
+        final String hostAddress = address.getAddress().getHostAddress();
+        final int port = address.getPort();
+
+        log.info("Listening on http://{}:{}", hostAddress, port);
     }
 
-    private void handleCreate(HttpExchange exchange) {
-        this.executorService.submit(() -> {
-            log.debug("Handling request to {}", exchange.getRequestURI());
-            try {
-                final String requestMethod = exchange.getRequestMethod();
-                if (!requestMethod.equals("GET")) {
-                    exchange.getResponseBody().close();
-                    exchange.sendResponseHeaders(405, -1);
-                    return;
-                }
-                final Map<RequestParameter, String> requestParams = this.parseQueryString(exchange.getRequestURI()
-                                                                                                  .getQuery());
-                final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 5);
-                final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 7);
-                final Option<Long> idOption = requestParams.get(RequestParameter.ID)
-                                                           .toTry()
-                                                           .map(Long::valueOf)
-                                                           .toOption();
-                final Option<OutputType> outputOption = requestParams.get(RequestParameter.OUTPUT)
-                                                                     .flatMap(OutputType::ofString);
-                final Headers responseHeaders = exchange.getResponseHeaders();
-                final AtomicBoolean needsRedirect = new AtomicBoolean(false);
-                final long id = idOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(System::nanoTime);
-                final OutputType output = outputOption.onEmpty(() -> needsRedirect.set(true))
-                                                      .getOrElse(OutputType.HTML);
-                if (needsRedirect.get()) {
-                    responseHeaders.add("Location",
-                                        "create?width=" + width + "&height=" + height + "&output=" + output.toString() +
-                                        "&id=" + id
-                    );
-                    exchange.sendResponseHeaders(302, -1);
-                    return;
-                }
-                final byte[] render;
-                try {
-                    final Labyrinth labyrinth = new Labyrinth(width, height, id);
-                    render = output.render(labyrinth);
-                } catch (Exception e) {
-                    responseHeaders.add("Content-type", "text/plain; charset=UTF-8");
-                    exchange.sendResponseHeaders(500, 0);
-                    final OutputStream responseBody = exchange.getResponseBody();
-                    responseBody.write(("Error: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
-                    responseBody.flush();
-                    return;
-                }
-                responseHeaders.add("Content-type", output.getContentType());
-                if (output.isAttachment()) {
-                    responseHeaders.add(
-                            "Content-disposition",
-                            String.format("attachment; filename=\"labyrinth-%dx%d-%d.%s\"",
-                                          width,
-                                          height,
-                                          id,
-                                          output.getFileExtension()
-                            )
-                    );
-                }
-                exchange.sendResponseHeaders(200, 0);
-                final OutputStream responseBody = exchange.getResponseBody();
-                responseBody.write(render);
-                responseBody.flush();
-            } catch (Exception e) {
-                log.error("FSCK!", e);
-            } finally {
-                exchange.close();
-            }
-        });
-    }
-
-    private void handleRender(final HttpExchange exchange) {
-        this.executorService.submit(() -> {
-            try {
-                log.debug("Handling request to {}", exchange.getRequestURI());
-                final String requestMethod = exchange.getRequestMethod();
-                if (!requestMethod.equals("POST")) {
-                    exchange.getResponseBody().close();
-                    exchange.sendResponseHeaders(405, -1);
-                    return;
-                }
-                final byte[] bytes = exchange.getRequestBody().readAllBytes();
-
-                final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes);
-
-                final OutputType output = exchange.getRequestHeaders()
-                                                  .get("Accept")
-                                                  .contains(OutputType.HTML.getContentType()) ?
-                                          OutputType.HTML :
-                                          OutputType.TEXT_PLAIN;
-                final byte[] render;
-                final Headers responseHeaders = exchange.getResponseHeaders();
-                try {
-                    render = output.render(labyrinth);
-                } catch (Exception e) {
-                    responseHeaders.add("Content-type", "text/plain; charset=UTF-8");
-                    exchange.sendResponseHeaders(500, 0);
-                    final OutputStream responseBody = exchange.getResponseBody();
-                    responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8));
-                    responseBody.flush();
-                    return;
-                }
-                responseHeaders.add("Content-type", output.getContentType());
-                exchange.sendResponseHeaders(200, 0);
-                final OutputStream responseBody = exchange.getResponseBody();
-                responseBody.write(render);
-                responseBody.flush();
-            } catch (Exception e) {
-                log.error("FSCK!", e);
-            } finally {
-                exchange.close();
-            }
-        });
-    }
-
-    public void stop() {
+    private void stop() {
         log.info("Stopping server ...");
-        this.httpServer.stop(5);
-        this.executorService.shutdown();
-        try {
-            if (!this.executorService.awaitTermination(5, TimeUnit.SECONDS)) {
-                log.warn("Timeout occurred while awaiting termination of executor service");
-            }
-        } catch (final InterruptedException e) {
-            log.error("Failed to await termination of executor service", e);
-        }
+        this.undertow.stop();
         log.info("Server stopped.");
     }
-
-    @NonNull
-    private Map<RequestParameter, String> parseQueryString(@Nullable final String query) {
-        if (query == null) {
-            return HashMap.empty();
-        }
-        HashMap<RequestParameter, String> result = HashMap.empty();
-        final String[] parts = query.split("&");
-        for (final String part : parts) {
-            final int split = part.indexOf('=');
-            if (split == -1) {
-                final Try<RequestParameter> tryKey = Try.of(() -> this.normalizeParameterName(part));
-                if (tryKey.isSuccess()) {
-                    result = result.put(tryKey.get(), null);
-                }
-            } else {
-                final String key = part.substring(0, split);
-                final String value = part.substring(split + 1);
-                final Try<RequestParameter> tryKey = Try.of(() -> this.normalizeParameterName(key));
-                if (tryKey.isSuccess()) {
-                    result = result.put(tryKey.get(), value);
-                }
-            }
-        }
-        return result;
-    }
-
-    private <T> T getOrDefault(@NonNull final Option<String> input,
-                               @NonNull final Function<String, T> mapper,
-                               @Nullable final T defaultValue) {
-        return input.toTry().map(mapper).getOrElse(defaultValue);
-    }
-
-    private RequestParameter normalizeParameterName(final String paramName) {
-        return RequestParameter.parseName(paramName).get();
-    }
-
-    private enum RequestParameter {
-        WIDTH("w", "width"),
-        HEIGHT("h", "height"),
-        ID("i", "id"),
-        OUTPUT("o", "output");
-        private final Set<String> names;
-
-        RequestParameter(@NonNull final String... names) {
-            this.names = HashSet.of(names);
-        }
-
-        static Option<RequestParameter> parseName(@Nullable final String name) {
-            if (name == null) {
-                return Option.none();
-            }
-            return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase));
-        }
-    }
-
-    private enum OutputType {
-        TEXT_PLAIN("text/plain; charset=UTF-8",
-                   labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
-                   "txt",
-                   false,
-                   "t",
-                   "text"
-        ),
-        HTML("text/html",
-             labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
-             "html",
-             false,
-             "h",
-             "html"
-        ),
-        PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "pdf", false, "p", "pdf"),
-        PDFFILE("application/pdf",
-                labyrinth -> PDFRenderer.newInstance().render(labyrinth),
-                "pdf",
-                true,
-                "f",
-                "pdffile"
-        ),
-        BINARY("application/octet-stream", SerializerDeserializer::serialize, "laby", true, "b", "binary");
-        @Getter
-        @NonNull
-        private final String contentType;
-        @NonNull
-        private final List<String> names;
-        @NonNull
-        private final Function<Labyrinth, byte[]> render;
-        @Getter
-        private final boolean attachment;
-        @Getter
-        @NonNull
-        private final String fileExtension;
-
-        OutputType(@NonNull final String contentType,
-                   @NonNull final Function<Labyrinth, byte[]> render,
-                   @NonNull final String fileExtension,
-                   final boolean attachment,
-                   @NonNull final String... names) {
-            this.contentType = contentType;
-            this.render = render;
-            this.fileExtension = fileExtension;
-            this.attachment = attachment;
-            this.names = List.of(names);
-        }
-
-        static Option<OutputType> ofString(@Nullable final String name) {
-            if (name == null) {
-                return Option.none();
-            }
-            final String nameLC = name.toLowerCase();
-            return Stream.of(values()).find(param -> param.names.contains(nameLC));
-        }
-
-        @Override
-        public String toString() {
-            return this.names.last();
-        }
-
-        byte[] render(@NonNull final Labyrinth labyrinth) {
-            return this.render.apply(labyrinth);
-        }
-    }
 }
diff --git a/src/main/java/ch/fritteli/labyrinth/server/Main.java b/src/main/java/ch/fritteli/labyrinth/server/Main.java
index ced13bd..9800d8c 100644
--- a/src/main/java/ch/fritteli/labyrinth/server/Main.java
+++ b/src/main/java/ch/fritteli/labyrinth/server/Main.java
@@ -1,11 +1,14 @@
 package ch.fritteli.labyrinth.server;
 
+import lombok.experimental.UtilityClass;
 import lombok.extern.slf4j.Slf4j;
 
 @Slf4j
+@UtilityClass
 public class Main {
+
     public static void main(String[] args) {
         LabyrinthServer.createAndStartServer()
-                .onEmpty(() -> log.error("Failed to create server. Stopping."));
+                .onFailure(e -> log.error("Failed to create server. Stopping.", e));
     }
 }
diff --git a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java
new file mode 100644
index 0000000..c1d08d9
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java
@@ -0,0 +1,94 @@
+package ch.fritteli.labyrinth.server;
+
+import ch.fritteli.labyrinth.generator.model.Labyrinth;
+import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer;
+import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer;
+import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer;
+import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer;
+import io.vavr.collection.List;
+import io.vavr.collection.Stream;
+import io.vavr.control.Option;
+import lombok.Getter;
+import lombok.NonNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.nio.charset.StandardCharsets;
+import java.util.function.Function;
+
+public enum OutputType {
+    TEXT_PLAIN("text/plain; charset=UTF-8",
+            "txt",
+            labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
+            false,
+            "t",
+            "text"),
+    HTML("text/html",
+            "html",
+            labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8),
+            false,
+            "h",
+            "html"),
+    PDF("application/pdf",
+            "pdf",
+            labyrinth -> PDFRenderer.newInstance().render(labyrinth).toByteArray(),
+            false,
+            "p",
+            "pdf"),
+    PDFFILE("application/pdf",
+            "pdf",
+            labyrinth -> PDFRenderer.newInstance().render(labyrinth).toByteArray(),
+            true,
+            "f",
+            "pdffile"),
+    BINARY("application/octet-stream",
+            "laby",
+            SerializerDeserializer::serialize,
+            true,
+            "b",
+            "binary");
+    @Getter
+    @NonNull
+    private final String contentType;
+    @Getter
+    @NonNull
+    private final String fileExtension;
+    @NonNull
+    private final Function<Labyrinth, byte[]> render;
+    @Getter
+    private final boolean attachment;
+    @Getter
+    @NonNull
+    private final List<String> names;
+
+    OutputType(@NonNull final String contentType,
+               @NonNull final String fileExtension,
+               @NonNull final Function<Labyrinth, byte[]> render,
+               final boolean attachment,
+               @NonNull final String... names) {
+        this.contentType = contentType;
+        this.render = render;
+        this.fileExtension = fileExtension;
+        this.attachment = attachment;
+        this.names = List.of(names);
+    }
+
+    @NonNull
+    public static Option<OutputType> ofString(@Nullable final String name) {
+        return Option.of(name)
+                .map(String::toLowerCase)
+                .flatMap(nameLC -> Stream.of(values())
+                        .find(param -> param.names.contains(nameLC)));
+
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return this.names.last();
+    }
+
+    @NonNull
+    public byte[] render(@NonNull final Labyrinth labyrinth) {
+        return this.render.apply(labyrinth);
+    }
+}
diff --git a/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java b/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java
index 58cee13..196b2c5 100644
--- a/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java
+++ b/src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java
@@ -10,7 +10,6 @@ import org.jetbrains.annotations.Nullable;
 
 import java.net.InetAddress;
 
-
 @AllArgsConstructor(access = AccessLevel.PRIVATE)
 @Slf4j
 @Value
@@ -39,7 +38,17 @@ public class ServerConfig {
     @NonNull
     private static InetAddress validateAddress(@Nullable final String address) {
         return Try.of(() -> InetAddress.getByName(address))
-                .getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause));
+                .getOrElseThrow(cause -> new ConfigurationException(
+                        "Invalid hostname/address: %s".formatted(address),
+                        cause
+                ));
+    }
+
+    private static int validatePort(final int port) {
+        if (port < 0 || port > 0xFFFF) {
+            throw new ConfigurationException("Port out of range (0..65535): %s".formatted(port));
+        }
+        return port;
     }
 
     private static int validatePort(@Nullable final String portString) {
@@ -48,14 +57,9 @@ public class ServerConfig {
             return 0;
         }
         return Try.of(() -> Integer.valueOf(portString))
-                .map(ServerConfig::validatePort)
-                .getOrElseThrow(cause -> new ConfigurationException("Failed to parse port specified in system property '" + SYSPROP_PORT + "': " + portString, cause));
-    }
-
-    private static int validatePort(final int port) {
-        if (port < 0 || port > 0xFFFF) {
-            throw new ConfigurationException("Port out of range (0..65535): " + port);
-        }
-        return port;
+                .getOrElseThrow(cause -> new ConfigurationException(
+                        "Failed to parse port specified in system property '%s': %s".formatted(SYSPROP_PORT, portString),
+                        cause
+                ));
     }
 }
diff --git a/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java b/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java
deleted file mode 100644
index d1cabfc..0000000
--- a/src/main/java/ch/fritteli/labyrinth/server/StaticResourcesFileHandler.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package ch.fritteli.labyrinth.server;
-
-import com.sun.net.httpserver.HttpExchange;
-import com.sun.net.httpserver.HttpHandler;
-import lombok.NonNull;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.pdfbox.io.IOUtils;
-import org.jetbrains.annotations.Nullable;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.nio.charset.StandardCharsets;
-import java.util.concurrent.ExecutorService;
-
-@Slf4j
-public class StaticResourcesFileHandler implements HttpHandler {
-
-    public static final String WEBASSETS_DIRECTORY = "webassets";
-    private final ExecutorService executorService;
-
-    public StaticResourcesFileHandler(final ExecutorService executorService) {
-        this.executorService = executorService;
-        log.debug("Created {}", this.getClass().getSimpleName());
-    }
-
-    private static void redirect(@NonNull final HttpExchange exchange, @NonNull final String target) throws IOException {
-        log.debug("Sending redirect to {}", target);
-        exchange.getResponseHeaders().add("Location", target);
-        exchange.sendResponseHeaders(302, -1);
-    }
-
-    private static void notFound(@NonNull final HttpExchange exchange, @NonNull final String path) throws IOException {
-        log.debug("Resource '{}' not found, replying with HTTP 404", path);
-        exchange.getResponseHeaders().add("Content-type", "text/plain; charset=utf-8");
-        exchange.sendResponseHeaders(404, 0);
-        exchange.getResponseBody().write("404 - Not found".getBytes(StandardCharsets.UTF_8));
-        exchange.getResponseBody().flush();
-    }
-
-    @NonNull
-    private static byte[] getBytes(@NonNull final String path) throws IOException {
-        final InputStream stream = StaticResourcesFileHandler.class.getClassLoader().getResourceAsStream(WEBASSETS_DIRECTORY + path);
-        if (stream == null) {
-            log.debug("Resource '{}' not found in classpath.", path);
-            return new byte[0];
-        }
-        final byte[] response = IOUtils.toByteArray(stream);
-        log.debug("Sending reply; {} bytes", response.length);
-        return response;
-    }
-
-    @Nullable
-    private static String getMimeType(@NonNull final String path) {
-        if (path.endsWith(".html")) {
-            return "text/html";
-        }
-        if (path.endsWith(".css")) {
-            return "text/css";
-        }
-        return null;
-    }
-
-    @Override
-    public void handle(@NonNull final HttpExchange exchange) throws IOException {
-        this.executorService.submit(() -> {
-            try {
-                final URI requestURI = exchange.getRequestURI();
-                final String path = requestURI.getPath();
-                log.debug("Handling request to {}", path);
-                if ("/".equals(path)) {
-                    redirect(exchange, "index.html");
-                    return;
-                }
-                if (!path.startsWith("/")) {
-                    notFound(exchange, path);
-                    return;
-                }
-                final String mimeType = getMimeType(path);
-                if (mimeType == null) {
-                    notFound(exchange, path);
-                    return;
-                }
-                final byte[] responseBytes = getBytes(path);
-                if (responseBytes.length == 0) {
-                    notFound(exchange, path);
-                    return;
-                }
-                log.debug("Serving {}{} with mimetype {}: {} bytes", WEBASSETS_DIRECTORY, path, mimeType, responseBytes.length);
-                exchange.getResponseHeaders().add("Content-type", mimeType);
-                exchange.sendResponseHeaders(200, 0);
-                exchange.getResponseBody().write(responseBytes);
-                exchange.getResponseBody().flush();
-            } catch (Exception e) {
-                log.error("FSCK!", e);
-            } finally {
-                exchange.close();
-            }
-        });
-    }
-}
diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java
new file mode 100644
index 0000000..198c7a4
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/server/handler/AbstractHttpHandler.java
@@ -0,0 +1,39 @@
+package ch.fritteli.labyrinth.server.handler;
+
+import io.undertow.server.HttpHandler;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.StatusCodes;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.UUID;
+
+@Slf4j
+public abstract class AbstractHttpHandler implements HttpHandler {
+    @Override
+    public final void handleRequest(@NonNull final HttpServerExchange exchange) {
+        final Instant start = Instant.now();
+        try (final MDC.MDCCloseable closeable = MDC.putCloseable("correlationId", UUID.randomUUID().toString())) {
+
+            if (exchange.isInIoThread()) {
+                log.debug("Dispatching request");
+                exchange.dispatch(this);
+                return;
+            }
+            try {
+                this.handle(exchange);
+            } catch (@NonNull final Exception e) {
+                log.error("Error handling request", e);
+                exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR)
+                        .getResponseSender()
+                        .send(StatusCodes.INTERNAL_SERVER_ERROR_STRING);
+            }
+            log.debug("Completed request in {}ms.", start.until(Instant.now(), ChronoUnit.MILLIS));
+        }
+    }
+
+    protected abstract void handle(@NonNull final HttpServerExchange exchange) throws Exception;
+}
diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java
new file mode 100644
index 0000000..344a907
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java
@@ -0,0 +1,76 @@
+package ch.fritteli.labyrinth.server.handler;
+
+import ch.fritteli.labyrinth.generator.model.Labyrinth;
+import ch.fritteli.labyrinth.server.OutputType;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.Headers;
+import io.undertow.util.HttpString;
+import io.undertow.util.StatusCodes;
+import io.vavr.Tuple2;
+import io.vavr.control.Try;
+import java.nio.ByteBuffer;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Deque;
+import java.util.Map;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+
+@Slf4j
+public class CreateHandler extends AbstractHttpHandler {
+
+    public static final String PATH_TEMPLATE = "/create/{output}";
+
+    @Override
+    protected void handle(@NonNull final HttpServerExchange exchange) {
+        final Instant start = Instant.now();
+        log.debug("Handling create request");
+        this.createLabyrinthFromRequestParameters(exchange.getQueryParameters())
+                .onFailure(e -> {
+                    log.error("Error creating Labyrinth from request", e);
+                    exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR)
+                            .setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING)
+                            .getResponseSender()
+                            .send(e.getMessage());
+                })
+                .forEach(tuple -> {
+                    final OutputType outputType = tuple._1();
+                    final Labyrinth labyrinth = tuple._2();
+                    final byte[] bytes;
+                    try {
+                        bytes = outputType.render(labyrinth);
+                    } catch (@NonNull final Exception e) {
+                        log.error("Error rendering Labyrinth", e);
+                        exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR)
+                                .setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING)
+                                .getResponseSender()
+                                .send("Error creating the Labyrinth. Please contact the administrator. Request id=%s".formatted(MDC.get("correlationId")));
+                        return;
+                    }
+                    final long durationMillis = start.until(Instant.now(), ChronoUnit.MILLIS);
+                    exchange.getResponseHeaders()
+                            .put(Headers.CONTENT_TYPE, outputType.getContentType())
+                            .put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(labyrinth.getRandomSeed()))
+                            .put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth()))
+                            .put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight()))
+                            .put(HttpString.tryFromString("X-Labyrinth-Generation-Duration-millis"), String.valueOf(durationMillis));
+                    if (outputType.isAttachment()) {
+                        exchange.getResponseHeaders()
+                                .put(Headers.CONTENT_DISPOSITION, "attachment; filename=\"labyrinth-%dx%d-%d.%s\"".formatted(
+                                        labyrinth.getWidth(),
+                                        labyrinth.getHeight(),
+                                        labyrinth.getRandomSeed(),
+                                        outputType.getFileExtension()
+                                ));
+                    }
+                    exchange.getResponseSender().send(ByteBuffer.wrap(bytes));
+                    log.debug("Create request handled in {}ms.", durationMillis);
+                });
+    }
+
+    @NonNull
+    private Try<Tuple2<OutputType, Labyrinth>> createLabyrinthFromRequestParameters(final Map<String, Deque<String>> queryParameters) {
+        return new ParametersToLabyrinthExtractor(queryParameters).createLabyrinth();
+    }
+}
diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java b/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java
new file mode 100644
index 0000000..a1cf854
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/server/handler/ParametersToLabyrinthExtractor.java
@@ -0,0 +1,55 @@
+package ch.fritteli.labyrinth.server.handler;
+
+import ch.fritteli.labyrinth.generator.model.Labyrinth;
+import ch.fritteli.labyrinth.server.OutputType;
+import io.vavr.Tuple;
+import io.vavr.Tuple2;
+import io.vavr.collection.Stream;
+import io.vavr.control.Option;
+import io.vavr.control.Try;
+import java.util.Deque;
+import java.util.Map;
+import java.util.Random;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+class ParametersToLabyrinthExtractor {
+
+    @NonNull
+    private final Map<String, Deque<String>> queryParameters;
+
+    @NonNull
+    Try<Tuple2<OutputType, Labyrinth>> createLabyrinth() {
+        final Option<OutputType> output = getParameterValue(RequestParameter.OUTPUT);
+        final Option<Integer> width = getParameterValue(RequestParameter.WIDTH);
+        final Option<Integer> height = getParameterValue(RequestParameter.HEIGHT);
+        final Option<Long> id = getParameterValue(RequestParameter.ID);
+
+        if (output.isEmpty()) {
+            return Try.failure(new IllegalArgumentException("Path parameter %s is required and must be one of: %s".formatted(
+                    RequestParameter.OUTPUT.getNames().mkString("'", " / ", "'"),
+                    Stream.of(OutputType.values())
+                            .flatMap(OutputType::getNames)
+                            .mkString(", ")
+            )));
+        }
+        if (width.isEmpty()) {
+            return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted(
+                    RequestParameter.WIDTH.getNames().mkString("'", " / ", "'")
+            )));
+        }
+        if (height.isEmpty()) {
+            return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted(
+                    RequestParameter.HEIGHT.getNames().mkString("'", " / ", "'")
+            )));
+        }
+
+        return Try.of(() -> Tuple.of(output.get(), new Labyrinth(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong()))));
+    }
+
+    @NonNull
+    private <T> Option<T> getParameterValue(@NonNull final RequestParameter parameter) {
+        return parameter.getParameterValue(this.queryParameters);
+    }
+}
diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java
new file mode 100644
index 0000000..52a61d5
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/server/handler/RenderHandler.java
@@ -0,0 +1,60 @@
+package ch.fritteli.labyrinth.server.handler;
+
+import ch.fritteli.labyrinth.generator.model.Labyrinth;
+import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer;
+import ch.fritteli.labyrinth.server.OutputType;
+import io.undertow.server.HttpServerExchange;
+import io.undertow.util.HeaderValues;
+import io.undertow.util.Headers;
+import io.undertow.util.StatusCodes;
+import java.nio.ByteBuffer;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class RenderHandler extends AbstractHttpHandler {
+
+    public static final String PATH_TEMPLATE = "/render/{output}";
+
+    @Override
+    public void handle(final HttpServerExchange exchange) {
+        log.debug("Handling render request");
+
+        if (exchange.isInIoThread()) {
+            exchange.dispatch(this);
+            return;
+        }
+        exchange.getRequestReceiver().receiveFullBytes((httpServerExchange, bytes) -> {
+            final OutputType output = this.getOutputType(httpServerExchange);
+            final byte[] render;
+            try {
+                final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes);
+                render = output.render(labyrinth);
+            } catch (final Exception e) {
+                log.error("Error rendering binary labyrinth data", e);
+                httpServerExchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR)
+                        .getResponseSender()
+                        .send("Error rendering labyrinth: %s".formatted(e.getMessage()));
+                return;
+            }
+            httpServerExchange
+                    .setStatusCode(StatusCodes.OK)
+                    .getResponseHeaders()
+                    .put(Headers.CONTENT_TYPE, output.getContentType());
+            httpServerExchange.getResponseSender()
+                    .send(ByteBuffer.wrap(render));
+        });
+    }
+
+    @NonNull
+    private OutputType getOutputType(@NonNull final HttpServerExchange httpServerExchange) {
+        return RequestParameter.OUTPUT.<OutputType>getParameterValue(httpServerExchange.getQueryParameters())
+                .getOrElse(() -> {
+                    final HeaderValues accept = httpServerExchange.getRequestHeaders().get(Headers.ACCEPT);
+                    if (accept.contains(OutputType.HTML.getContentType())) {
+                        return OutputType.HTML;
+                    }
+                    return OutputType.TEXT_PLAIN;
+                });
+    }
+}
diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/RequestParameter.java b/src/main/java/ch/fritteli/labyrinth/server/handler/RequestParameter.java
new file mode 100644
index 0000000..314ee22
--- /dev/null
+++ b/src/main/java/ch/fritteli/labyrinth/server/handler/RequestParameter.java
@@ -0,0 +1,54 @@
+package ch.fritteli.labyrinth.server.handler;
+
+import ch.fritteli.labyrinth.server.OutputType;
+import io.vavr.Tuple2;
+import io.vavr.collection.HashMap;
+import io.vavr.collection.HashSet;
+import io.vavr.collection.Set;
+import io.vavr.control.Option;
+import io.vavr.control.Try;
+import java.util.Deque;
+import java.util.Map;
+import java.util.function.Function;
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+enum RequestParameter {
+    WIDTH(p -> Try.of(() -> Integer.parseInt(p))
+            .toOption()
+            .onEmpty(() -> log.debug("Unparseable value for parameter 'width': '{}'", p)), "w", "width"),
+    HEIGHT(p -> Try.of(() -> Integer.parseInt(p))
+            .toOption()
+            .onEmpty(() -> log.debug("Unparseable value for parameter 'height': '{}'", p)), "h", "height"),
+    ID(p -> Try.of(() -> Long.parseLong(p))
+            .toOption()
+            .onEmpty(() -> log.debug("Unparseable value for parameter 'id': '{}'", p)), "i", "id"),
+    OUTPUT(p -> OutputType.ofString(p)
+            .onEmpty(() -> log.debug("Unparseable value for parameter 'output': '{}'", p)), "o", "output");
+    @NonNull
+    private final Function<String, Option<?>> extractor;
+    @Getter
+    @NonNull
+    private final Set<String> names;
+
+    RequestParameter(@NonNull final Function<String, Option<?>> extractor, @NonNull final String... names) {
+        this.extractor = extractor;
+        this.names = HashSet.of(names);
+    }
+
+    @NonNull
+    Option<?> extractParameterValue(@NonNull final String parameter) {
+        return this.extractor.apply(parameter);
+    }
+
+    @NonNull
+    public <T> Option<T> getParameterValue(@NonNull final Map<String, Deque<String>> queryParameters) {
+        return (Option<T>) HashMap.ofAll(queryParameters)
+                .filterKeys(this.names::contains)
+                .flatMap(Tuple2::_2)
+                .flatMap(this::extractParameterValue)
+                .headOption();
+    }
+}
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index 4bba3c1..d660474 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -1,17 +1,17 @@
 <?xml version="1.0" encoding="utf-8" ?>
 <configuration>
-	<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/>
+    <shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook"/>
 
-	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
-		<!-- encoders are by default assigned the type
-		ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
-		<encoder>
-			<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
-		</encoder>
-	</appender>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <!-- encoders are by default assigned the type
+        ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
+        <encoder>
+            <pattern>%d{HH:mm:ss.SSS} %-5level %X{correlationId} [%thread] %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
 
-	<root level="info">
-		<appender-ref ref="STDOUT"/>
-	</root>
-	<logger name="ch.fritteli.labyrinth.server.StaticResourcesFileHandler" level="debug"/>
+    <root level="info">
+        <appender-ref ref="STDOUT"/>
+    </root>
+    <logger name="ch.fritteli.labyrinth.*" level="debug"/>
 </configuration>
diff --git a/src/main/resources/webassets/index.html b/src/main/resources/webassets/index.html
deleted file mode 100644
index 3444b42..0000000
--- a/src/main/resources/webassets/index.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <title>Labyrinth Generator</title>
-    <link rel="stylesheet" href="style.css">
-</head>
-<body>
-<div class="content">
-    <h1>Labyrinth Generator</h1>
-    <p>Enter some values, click the "Create!" button and see what happens!</p>
-    <form action="/create" method="get">
-        <div class="inputs-wrapper">
-            <label for="width">Width:</label><input id="width" name="width" type="number" min="1" required>
-            <label for="height">Height:</label><input id="height" name="height" type="number" min="1" required>
-            <label for="output">Output format:</label>
-            <select id="output" name="output" required>
-                <option label="HTML Document" value="html"></option>
-                <option label="Plain text" value="text"></option>
-                <option label="PDF Document" value="pdf"></option>
-                <option label="PDF Document (Download)" value="pdffile"></option>
-                <option label="Binary" value="binary"></option>
-            </select>
-            <label for="id">Seed (optional):</label><input id="id" name="id" type="number">
-        </div>
-        <div class="controls-wrapper">
-            <button type="submit" class="primary">Create!</button>
-            <button type="reset">Reset form</button>
-        </div>
-    </form>
-</div>
-</body>
-</html>
diff --git a/src/main/resources/webassets/style.css b/src/main/resources/webassets/style.css
deleted file mode 100644
index 153f8e8..0000000
--- a/src/main/resources/webassets/style.css
+++ /dev/null
@@ -1,51 +0,0 @@
-:root {
-    --color-background: #181a1b;
-    --color-foreground: #e8e6e3;
-    --color-border: #5d6164;
-    --color-background-highlight: #292b2c;
-    --color-foreground-highlight: #f8f7f4;
-    --color-border-highlight: #6e7275;
-}
-
-body {
-    display: flex;
-    font-family: sans-serif;
-    justify-content: center;
-}
-
-body, button, input, select {
-    background-color: var(--color-background);
-    color: var(--color-foreground);
-}
-
-button, input, select {
-    border: 1px solid var(--color-border);
-}
-
-button:active, input:active, select:active,
-button:focus, input:focus, select:focus,
-button:hover, input:hover, select:hover {
-    background-color: var(--color-background-highlight);
-    border-color: var(--color-border-highlight);
-    color: var(--color-foreground-highlight);
-}
-
-button.primary {
-    background-color: #ffcc00;
-    border-color: #eebb00;
-    color: var(--color-background);
-}
-
-.content {
-    width: 75%;
-}
-
-.content h1 {
-    text-align: center;
-}
-
-.inputs-wrapper {
-    display: grid;
-    grid-row-gap: 1em;
-    grid-template-columns: 0.5fr 1fr;
-}