On this page
Build a Vue.js App
Vue.js is a progressive front-end JavaScript framework. It provides tools and features for creating dynamic and interactive user interfaces.
In this tutorial we'll build a simple Vue.js app with Vite and Deno. The app will display a list of dinosaurs. When you click on one, it'll take you to a dinosaur page with more details. You can see the finished app on GitHub.
You can see a live version of the app on Deno Deploy.
Create a Vue.js app with Vite and Deno Jump to heading
We'll use Vite to scaffold a basic Vue.js app. In your terminal, run the following command to create a new .js app with Vite:
deno run -A npm:create-vite
When prompted, give your app a name and select Vue
from the offered frameworks
and TypeScript
as a variant.
Once created, cd
into your new project and run the following command to
install dependencies:
deno install
Then, run the following command to serve your new Vue.js app:
deno task dev
Deno will run the dev
task from the package.json
file which will start the
Vite server. Click the output link to localhost to see your app in the browser.
Configure the formatter Jump to heading
deno fmt
supports Vue files with the
--unstable-component
flag. To use it, run this command:
deno fmt --unstable-component
To configure deno fmt
to always format your Vue files, add this at the top
level of your deno.json
file:
"unstable": ["fmt-component"]
Add a backend API Jump to heading
We'll build an API server using Deno and Oak. This will be where we get our dinosaur data.
In the root directory of your project, create an api
folder. In that folder,
create a data.json
, which will contain the hard coded dinosaur data.
Copy and paste
this json file
into the api/data.json
file. (If you were building a real app, you would
probably fetch this data from a database or an external API.)
We're going to build out some API routes that return dinosaur information. We'll need Oak for the HTTP server and CORS middleware to enable CORS.
Add the dependencies to your deno.json
file by updating the imports section:
{
"imports": {
"@oak/oak": "jsr:@oak/oak@^17.1.5",
"@tajpouria/cors": "jsr:@tajpouria/cors@^1.2.1",
"vue-router": "npm:vue-router@^4.5.1"
}
}
Next, create api/main.ts
and import the required modules and create a new
Router
instance to define some routes:
import { Application, Router } from "@oak/oak";
import { oakCors } from "@tajpouria/cors";
import routeStaticFilesFrom from "./util/routeStaticFilesFrom.ts";
import data from "./data.json" with { type: "json" };
export const app = new Application();
const router = new Router();
After this, in the same file, we'll define two routes. One at /api/dinosaurs
to return all the dinosaurs, and /api/dinosaurs/:dinosaur
to return a specific
dinosaur based on the name in the URL:
router.get("/api/dinosaurs", (context) => {
context.response.body = data;
});
router.get("/api/dinosaurs/:dinosaur", (context) => {
if (!context?.params?.dinosaur) {
context.response.body = "No dinosaur name provided.";
}
const dinosaur = data.find((item) =>
item.name.toLowerCase() === context.params.dinosaur.toLowerCase()
);
context.response.body = dinosaur ?? "No dinosaur found.";
});
At the bottom of the same file, attach the routes we just defined to the application. We also must include the static file server, and finally we'll start the server listening on port 8000:
app.use(oakCors());
app.use(router.routes());
app.use(router.allowedMethods());
app.use(routeStaticFilesFrom([
`${Deno.cwd()}/dist`,
`${Deno.cwd()}/public`,
]));
if (import.meta.main) {
console.log("Server listening on port http://localhost:8000");
await app.listen({ port: 8000 });
}
You'll also need to create the api/util/routeStaticFilesFrom.ts
file to serve
static files:
import { Context, Next } from "@oak/oak";
// Configure static site routes so that we can serve
// the Vite build output and the public folder
export default function routeStaticFilesFrom(staticPaths: string[]) {
return async (context: Context<Record<string, object>>, next: Next) => {
for (const path of staticPaths) {
try {
await context.send({ root: path, index: "index.html" });
return;
} catch {
continue;
}
}
await next();
};
}
You can run the API server with
deno run --allow-env --allow-net --allow-read api/main.ts
. We'll create a task
to run this command in the background and update the dev task to run both the
Vue app and the API server.
Update your package.json
scripts to include the following:
{
"scripts": {
"dev": "deno task dev:api & deno task dev:vite",
"dev:api": "deno run --allow-env --allow-net api/main.ts",
"dev:vite": "deno run -A npm:vite",
"build": "deno run -A npm:vite build",
"server:start": "deno run -A --watch ./api/main.ts",
"serve": "deno run build && deno run server:start",
"preview": "vite preview"
}
}
Make sure your vite.config.ts
includes the Deno plugin and proxy configuration
for development:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import deno from "@deno/vite-plugin";
export default defineConfig({
server: {
port: 3000,
proxy: {
"/api": {
target: "http://localhost:8000",
changeOrigin: true,
},
},
},
plugins: [vue(), deno()],
optimizeDeps: {
include: ["react/jsx-runtime"],
},
});
If you run npm run dev
now and visit localhost:8000/api/dinosaurs
, in your
browser you should see a JSON response of all of the dinosaurs.
Build the frontend Jump to heading
The entrypoint and routing Jump to heading
In the src
directory, you'll find a main.ts
file. This is the entry point
for the Vue.js app. Our app will have multiple route, so we'll need a router to
do our client-side routing. We'll use the official
Vue Router for this.
Update src/main.ts
to import and use the router:
import { createApp } from "vue";
import router from "./router/index.ts";
import "./style.css";
import App from "./App.vue";
createApp(App)
.use(router)
.mount("#app");
Add the Vue Router module to the project by updating your deno.json
imports:
{
"imports": {
"@oak/oak": "jsr:@oak/oak@^17.1.5",
"@tajpouria/cors": "jsr:@tajpouria/cors@^1.2.1",
"vue-router": "npm:vue-router@^4.5.1"
}
}
Next, create a router
directory in the src
directory. In it, create an
index.ts
file with the following content:
import { createRouter, createWebHistory } from "vue-router";
import HomePage from "../components/HomePage.vue";
import Dinosaur from "../components/Dinosaur.vue";
export default createRouter({
history: createWebHistory("/"),
routes: [
{
path: "/",
name: "Home",
component: HomePage,
},
{
path: "/:dinosaur",
name: "Dinosaur",
component: Dinosaur,
props: true,
},
],
});
This will set up a router with two routes: /
and /:dinosaur
. The HomePage
component will be rendered at /
and the Dinosaur
component will be rendered
at /:dinosaur
.
Finally, you can delete all of the code in the src/App.vue
file to and update
it to include only the <RouterView>
component:
<template>
<RouterView />
</template>
The components Jump to heading
Vue.js splits the frontend UI into components. Each component is a reusable piece of code. We'll create three components: one for the home page, one for the list of dinosaurs, and one for an individual dinosaur.
Each component file is split into three parts: <script>
, <template>
, and
<style>
. The <script>
tag contains the JavaScript logic for the component,
the <template>
tag contains the HTML, and the <style>
tag contains the CSS.
In the /src/components
directory, create three new files: HomePage.vue
,
Dinosaurs.vue
, and Dinosaur.vue
.
The Dinosaurs component Jump to heading
The Dinosaurs
component will fetch the list of dinosaurs from the API we set
up earlier and render them as links using the
RouterLink
component from Vue Router.
(Because we are making a TypeScript project, don't forget to specify the
lang="ts"
attribute on the script tag.) Add the following code to the
Dinosaurs.vue
file:
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
async setup() {
const res = await fetch("/api/dinosaurs");
const dinosaurs = await res.json() as Dinosaur[];
return { dinosaurs };
},
});
</script>
<template>
<span v-for="dinosaur in dinosaurs" :key="dinosaur.name">
<RouterLink
:to="{ name: 'Dinosaur', params: { dinosaur: `${dinosaur.name.toLowerCase()}` } }"
>
{{ dinosaur.name }}
</RouterLink>
</span>
</template>
This code uses the Vue.js
v-for directive to
iterate over the dinosaurs
array and render each dinosaur as a RouterLink
component. The :to
attribute of the RouterLink
component specifies the route
to navigate to when the link is clicked, and the :key
attribute is used to
uniquely identify each dinosaur.
The Homepage component Jump to heading
The homepage will contain a heading and then it will render the Dinosaurs
component. Add the following code to the HomePage.vue
file:
<script setup lang="ts">
import Dinosaurs from "./Dinosaurs.vue";
</script>
<template>
<h1>Welcome to the Dinosaur App! 🦕</h1>
<p>Click on a dinosaur to learn more about them</p>
<Suspense>
<template #default>
<Dinosaurs />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
Because the Dinosaurs
component fetches data asynchronously, use the
Suspense
component to
handle the loading state.
The Dinosaur component Jump to heading
The Dinosaur
component will display the name and description of a specific
dinosaur and a link to go back to the full list.
First, we'll set up some types for the data we'll be fetching. Create a
types.ts
file in the src
directory and add the following code:
type Dinosaur = {
name: string;
description: string;
};
type ComponentData = {
dinosaurDetails: null | Dinosaur;
};
Then update the Dinosaur.vue
file:
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
props: { dinosaur: String },
data(): ComponentData {
return {
dinosaurDetails: null,
};
},
async mounted() {
const res = await fetch(
`/api/dinosaurs/${this.dinosaur}`,
);
this.dinosaurDetails = await res.json();
},
});
</script>
<template>
<h1>{{ dinosaurDetails?.name }}</h1>
<p>{{ dinosaurDetails?.description }}</p>
<RouterLink to="/">ðŸ Back to all dinosaurs</RouterLink>
</template>
This code uses the props
option to define a prop named dinosaur
that will be
passed to the component. The mounted
lifecycle hook is used to fetch the
details of the dinosaur based on the dinosaur
prop and store them in the
dinosaurDetails
data property. This data is then rendered in the template.
Run the app Jump to heading
Now that we've set up the frontend and backend, we can run the app. In your terminal, run the following command:
npm run dev
This will start both the Deno API server on port 8000 and the Vite development server on port 3000. The Vite server will proxy API requests to the Deno server.
Visit http://localhost:3000
in your browser to see the app. Click on a
dinosaur to see more details!
You can see a live version of the app on Deno Deploy.
npm run serve
This will build the Vue app and serve everything from the Deno server on port 8000.
Build and deploy Jump to heading
We set up the project with a serve
task that builds the React app and serves
it with the Oak backend server. Run the following command to build and serve the
app in production mode:
deno run build
deno run serve
This will:
- Build the Vue app using Vite (output goes to
dist/
) - Start the Oak server which serves both the API and the built Vue app
Visit localhost:8000
in your browser to see the production version of the app!
You can deploy this app to your favorite cloud provider. We recommend using Deno Deploy for a simple and easy deployment experience. You can deploy your app directly from GitHub, simply create a GitHub repository and push your code there, then connect it to Deno Deploy.
Create a GitHub repository Jump to heading
Create a new GitHub repository, then initialize and push your app to GitHub:
git init -b main
git remote add origin https://github.com/<your_github_username>/<your_repo_name>.git
git add .
git commit -am 'my vue app'
git push -u origin main
Deploy to Deno Deploy Jump to heading
Once your app is on GitHub, you can deploy it on the Deno DeployEA dashboard. Deploy my app
For a walkthrough of deploying your app, check out the Deno Deploy tutorial.
🦕 Now that you can run a Vue app in Deno with Vite you're ready to build real world applications!