Building the Application
Learning objectives
- You know of the possibility to build an application that consists of both the server-side functionality and the client-side functionality.
- You know how to build a SvelteKit application.
So far, when working on our applications since the introduction of the client-side functionality, we have been running the server-side functionality and the client-side functionality separately using Docker Compose. This is a great way to develop the application. For deployment, depending on how the application works, we could also merge the server-side and client-side functionality together where the client-side functionality would be server from the Deno server.
Here, we'll look into this through an example.
Sample application
As the example application, we have a project with server-side functionality and client-side functionality. The server-side functionality is in a folder called api
, while the client-side functionality is in folder called ui
. The high-level folder structure looks as follows.
tree --dirsfirst -L 1
.
├── api
└── ui
For demonstration purposes, to avoid clutter, we do not have
docker-compose.yml
,flyway
or anything that we'd normally use for developing the project.
On the server, in the folder api
, we use a simple Hono application that returns a JSON document. The application consists of three files, app.js
, app-run.js
, and deps.js
.
The app.js
looks as follows.
import { Hono } from "./deps.js";
const app = new Hono();
app.get("/api/random", (c) => c.json({
random: Math.random()
}));
export default app;
The app-run.js
looks as follows.
import app from "./app.js"
Deno.serve(app.fetch);
And the deps.js
looks as follows.
export { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
On the client-side, in the folder ui
, we have a SvelteKit application that has been created using the Skeleton project template as outlined in the first part of the chapter Introduction to Svelte.
The application has been modified to show a button that, when pressed, makes a fetch
query to the /api/random
endpoint on the server and shows the contents to the user.
<script>
let random = $state(null);
const fetchRandom = async () => {
const response = await fetch("/api/random");
const data = await response.json();
random = data.random;
}
</script>
<button on:click={fetchRandom}>New random</button>
<p>Random: {random}</p>
Size does not matter
The application size does not matter. The principles that we look into here work as well for a small application as they do for a large application.
Building the SvelteKit application
To build the SvelteKit application as a collection of static assets, we use the static site generation functionality provided by SvelteKit. To take this into use, we need to add the @sveltejs/adapter-static
adapter to our application. In the folder that contains the SvelteKit application (i.e., ui
), run the command npm i -D @sveltejs/adapter-static
.
cd ui
npm i -D @sveltejs/adapter-static
added 54 packages, and audited 55 packages in 8s
4 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Next, we need to modify the Svelte configuration svelte.config.js
to use the adapter. We can do this by changing the adapter-auto
to adapter-static
.
import adapter from '@sveltejs/adapter-static';
const config = {
kit: {
adapter: adapter()
}
};
export default config;
In addition, we need to explicitly state that all of the contents should be pre-rendered. To do so, create a file +layout.js
to the routes
folder in src
and place the following line to the file. This will tell svelte to prerender the application.
export const prerender = true;
Now, we can run the npm run build
command in the ui
folder of the application (i.e., in the folder with client-side functionality). This will create a directory called build
that contains the the application as static assets. We can either use npm run build
or vite build
, and the result is the same. The following example outlines the output of our application.
cd ui
npm run build
..
..
> Using @sveltejs/adapter-static
Wrote site to "build"
✔ done
✓ built in 2.58s
cd build
tree --dirsfirst
.
├── _app
│ ├── immutable
│ │ ├── chunks
│ │ │ ├── disclose-version.e0d88f06.js
│ │ │ ├── main-client.a136a0ea.js
│ │ │ ├── proxy.3fed43a0.js
│ │ │ ├── runtime.2bbb1b43.js
│ │ │ └── singletons.dbf485d0.js
│ │ ├── entry
│ │ │ ├── app.70abf6d6.js
│ │ │ └── start.045c2c7c.js
│ │ └── nodes
│ │ ├── 0.4e8439c9.js
│ │ ├── 1.7ffb7745.js
│ │ └── 2.b3d6af30.js
│ └── version.json
├── favicon.png
└── index.html
5 directories, 13 files
Now, the build
folder contains a client-side application that can be served from a web server.
Serving files from Deno application
Let's next modify our Deno application to allow serving the client-side application files. For serving the files, we'll use Hono's serveStatic middleware. First, modify deps.js
to match the following.
export { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
export { serveStatic } from "https://deno.land/x/hono@v3.12.11/middleware.ts";
Then, copy the build
folder from the SvelteKit application to the root folder of the Deno application. Let's use the name static
as the folder of the statically built resources. This can be done with the following command (which is executed in the folder that contains the Deno application).
tree --dirsfirst
.
├── app.js
├── app-run.js
└── deps.js
cp -r ../ui/build/ static
The build
directory of the SvelteKit application has now been copied as the static
directory of our server-side Deno application. The directory static
contains an index.html
file which is the entrypoint to the application, an favicon.png
file that is used as the favicon, and a _app
directory that contains the client-side application code. Let's modify our server-side application to serve these files.
Note that the files may differ depending on your application. Looking into the folder is always sensible!
import { Hono, serveStatic } from "./deps.js";
const app = new Hono();
app.use("/", serveStatic({ path: './static/index.html' }))
app.use("/favicon.png", serveStatic({ path: './static/favicon.png' }))
app.use("/_app/*", serveStatic({ root: './static/' }))
app.get("/api/random", (c) => c.json({
random: Math.random()
}));
export default app;
Above, we use the serveStatic
middleware for three separate routes. The first route serves the index.html
file when the root path of the application is requested. The second route serves the favicon.png
file when the /favicon.png
endpoint is requested. Finally, the third route serves the client-side application when anything under /_app/
is requested.
With these changes in place, the Deno application contains both the client-side functionality and the server-side functionality. The application can now be as usual with the deno run
command.
Naturally, whenever the client-side functionality changes, we would have to build the client-side application and copy it to the Deno server.
Try it out!
You could deploy the above application to Deno Deploy following the guidelines in the chapter on Deployment.
Creating a runnable Deno application
This is optional and does not bring e.g. performance benefits.
In addition to building the client-side code, we can also build the server-side code into a single executable file. This can be done using the deno compile
command (see Deno's documentation on Compiling Executables). The command is given the permission flags that the application needs to run. In addition, the command takes the --output
option that defines the name of the executable file.
As an example, the command deno compile --output random-api --allow-net --allow-read=static app-run.js
would create an executable file called random-api
that has the permissions to read the static
folder and to access the network. The executable would be created from the app-run.js
file.
tree --dirsfirst -L 1
.
├── static
├── app.js
├── app-run.js
└── deps.js
1 directory, 3 files
deno compile --output random-api --allow-net --allow-read=static app-run.js
Compile file:///path-to-folder/app-run.js to random-app
tree --dirsfirst -L 1
.
├── static
├── app.js
├── app-run.js
├── deps.js
└── random-app
1 directory, 4 files
Now, we could run the application using the ./random-api
command without a need for the Deno runtime.
./random-api
Listening on http://localhost:8000/
Note that the command only bundles the Deno runtime and the dependencies into the executable. Static files (e.g. the contents from the folder static
) are not bundled into the executable. This means that the static files need to be available in the same folder as the executable when the program is run (if they are needed by the program).
The
deno compile
is a nice to know feature in case one would wish to create a setup that does not have Deno installed. Performance-wise, there are no differences between creating the executable and running it when compared to running the application withdeno run
command.