Setting up an Express + Typescript Server with Vue + Vite
A Beginner’s Guide to Full-Stack Development
Introduction
Venturing into backend development can feel like stepping into uncharted territory, especially for us frontend developers. As a self-proclaimed "frontend girlie," I'll admit, I felt apprehensive about diving into backend development. However, after offloading some of my project's solely client-side logic to the backend, I discovered that things server-side aren’t as intimidating as I imagined.
If you resonate with my apprehensions about backend development, then you're in the right place. In this blog, I'll guide you through integrating an Express server with TypeScript into your frontend project.
This comprehensive guide is tailored for beginners and will focus on:
Setting up an Express server with TypeScript
Establishing communication between the client and server
Configuring Vite to proxy requests
Let's dive in and bridge the gap between frontend and backend. 🚀
Getting Started
To access the full source code for this tutorial, visit the GitHub repository here. Feel free to clone, fork, or star the repository for future reference and experimentation. Remember to add the environment variables for the server and client.
Before we start, ensure that you have Node.js installed on your computer. You can check if Node.js is installed by running the command node -v
in your terminal. If it is installed, it will display the version you have. Otherwise, you can install Node.js by clicking here.
Setting up the Server
The first step in setting up our backend is selecting the right tech stack. We'll be using Express.js, a popular Node.js framework known for its simplicity and flexibility. Additionally, we'll leverage TypeScript to bring static typing to our server-side codebase.
Initializing the Project
We’ll begin by creating a new directory and initializing a new Node.js project using npm
. Once initialized, we’ll install the necessary dependencies, including Express and TypeScript, by running the respective commands.
Create a New Directory.
We’ll start by creating a new directory for our starter project. You can do this using the terminal or file explorer of your operating system.
mkdir express-starter
Navigate to the Project Directory.
Once the directory is created, navigate into it using the
cd
command.cd express-starter
Create Server and Client Folders.
Once inside
express-starter
directory, create two folders:client
andserver
then navigate into theserver
folder.mkdir server client cd server
Initialize a New Node.js Project.
Use
npm init
to initialize a new Node.js project. This will generate apackage.json
file, which will store metadata about the project and its dependencies. You can either follow the prompts or use the-y
flag to accept the default values for all prompts.npm init -y
Install Dependencies.
Now, we’ll install the necessary dependencies for our project.
# dependencies npm install express cors dotenv # development dependencies npm install -D typescript @types/cors @types/node @types/express nodemon
Lets briefly look what each dependency does.
express
: The web framework for Node.js that we'll use to build our server.typescript
: The TypeScript compiler and language. We want our project to be type safe and catch bugs before runtime.cors
: Cross-Origin Resource Sharing to allow cross-origin requests and ensure that our backend APIs can be accessed securely from the client despite running on different ports.dotenv
: Loads environment variables from.env
file.nodemon
: Nodemon automatically restarts our node application when it detects any changes. This means that we won’t have to stop and restart our app in order for any changes to take effect.@types/node
,@types/cors
and@types/express
: Type definitions for Node.js, Cors and Express to enable TypeScript support.
Dependency vs Development Dependency
We added some dev dependencies during our installations by using the -D
flag, but why did we need to? Dev dependencies are modules which are only required during development whereas dependencies are required at runtime.
Dependencies
Dependencies are essential packages needed for our application to function properly. When we run npm install
, these packages are installed. They're listed in the dependencies
section of the package.json
file. Without them, our app won't work when deployed.
Dev Dependencies
DevDependencies, on the other hand, are only necessary for development and testing purposes. They're not required for the app to run normally, but they're crucial for tasks like building, testing, and linting the code. These are specified in the devDependencies
section of package.json
.
Generate a
tsconfig.json
Create a
tsconfig.json
file to configure TypeScript. This file specifies how TypeScript should compile our code. Runnpx tsconfig.json
then selectNode
. This command generates atsconfig.json
file with some default settings.npx tsconfig.json
Create Source Files and Update package.json
Next, create a
src
directory. Within thesrc
directory, we'll create our main file,main.ts
# create src folder mkdir src # go into the folder cd src # create .ts file touch main.ts # exit src and return to server directory cd ..
Update the entry point in
package.json
to usemain.js
instead ofindex.js
."main": "main.js",
Create a
.env
File.We’ll add a
.env
file to store the environment variables for configuration (e.g. API keys). This file should be kept out of version control to prevent sensitive information from being exposed. Create a.env
file in the root of theserver
folder.touch .env
Create a
.gitignore
File.We’ll add a
.gitignore
to specify which files and directories should be ignored by version control to avoid committing unnecessary files. Create a.gitignore
file in the root of theserver
folder. For now we’ll addnode_modules
and.env*
. The*
tells Git to ignore any file or folder that starts with.env
..env* node_modules/
At this point your project structure might look something like this:
express-starter/ ├── client/ └── server/ ├── node_modules/ └── src/ └── main.ts ├── .env ├── .gitignore ├── package.json ├── tsconfig.json
Now that we’ve done the basic config, let’s create our server.
Creating the Server
Inside the src/main.ts
file. Add the following snippet.
// server/src/main.ts
import cors from 'cors';
import 'dotenv/config';
import express from 'express';
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const PORT = process.env.PORT || 3001;
app.get('/api', (_req, res) => {
res.status(200).json({ message: 'Hello from the server!' });
});
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
This is a basic express server. This app starts a server and listens on the specified port for connections. The app responds with “Hello from the server!” for requests to the /api
route. Let’s understand what the code does.
- Middleware Setup
app.use()
is how we register middlewares. Middlewares are special functions that the server runs before handling any specific request. They work behind the scenes, between the moment the server receives a request and when it sends back a response to the client.
In our case, we've registered a few global middlewares:
cors: This middleware allows our server to accept requests from different sources/origins.
express.json(): Parses incoming requests as JSON.
express.urlencoded({ extended: true }): Parses form data in requests.
- Route Setup
app.get('/api')
sets up a route handler for GET requests specifically to the /api
URL. When a GET request is made to this route, the server responds with a status code of 200 and the message "Hello from the server!".
- Server Initialization
app.listen()
starts the Express app by listening on the specified port (PORT
). If no port is specified in the environment variables, it defaults to port 3001.
With this setup, we are a step closer to running our server.
Configuring Nodemon with TypeScript
To execute our main.ts
we can run node src/main.ts
within the server
directory, however we’ll eventually get an error saying Unknown file extension ".ts”
.
Node and TypeScript
When you run node src/main.ts
, you might expect Node.js to execute the TypeScript file directly. However, Node.js doesn't understand TypeScript natively; it only understands JavaScript.
TypeScript is a superset of JavaScript, therefore our TypeScript code needs to be compiled into JavaScript before it can be executed by Node.js. This compilation process transforms the .ts
files into equivalent .js
files that Node can understand.
To run our server code, we first need to compile our TypeScript code into JavaScript using the TypeScript compiler (tsc
). This will generate a dist/main.js
file, which contains the compiled JavaScript code.
# compile all TypeScript files to JavaScript
npx tsc
# run the generated JavaScript file
node dist/main.js
In the commands above, npx tsc
invokes the TypeScript compiler (tsc
) to compile all TypeScript files in the project into JavaScript. The resulting JavaScript files are output to the dist
directory. We can run the generated JavaScript file using Node.js.
The output directory for compiled JavaScript files is specified in the tsconfig.json
file using the outDir
property.
// server/tsconfig.json
{
"compilerOptions": {
"outDir": "./dist"
}
}
Finally, we need to update our .gitignore
file to include the dist
folder. This ensures that the compiled JavaScript files are not included in version control.
// server/.gitignore
node_modules
dist
.env*
Detecting Changes with Nodemon
If we make changes to any TypeScript files, we'll have to manually compile and run those files again with npx tsc
and node dist/main.js
. This repetition quickly become tedious. To streamline this process, we can use a tool called nodemon.
Nodemon is a tool that helps develop Node.js based applications by automatically restarting the node application when file changes in the directory are detected. This saves us the hassle of manually stopping and restarting the server every time we make changes to our code.
We already installed nodemon, so now we just need to configure it.
Configuring Nodemon
To configure nodemon we can add a nodemonConfig
to our package.json
.
// server/package.json
"scripts": {
"dev" : "nodemon"
},
"nodemonConfig":{
"watch": [
"src"
],
"exec": "tsc && node ./dist/main.js",
"ext": "ts,js,json"
},
The nodemonConfig
section tells nodemon how to behave when monitoring file changes:
watch
: Instructs nodemon to watch for any changes in thesrc
folder.ext
: Specifies the file extensions to watch for changes (ts
,js
,json
).exec
: Defines the command to run when changes are detected. In this case, it runstsc
to compile TypeScript files and then executesnode ./dist/main.js
to start the server.
Now, we can simply run npm run dev
to start the server with nodemon, automating the process of monitoring file changes and restarting the server accordingly. With these changes our server is ready to receive requests. If you have an API platform like Postman you can try it by sending a GET
request to http://localhost:3001/api, or just visit the link in your browser.
Setting up the Client
Our server is ready to accept requests from the client. On the frontend we’ll use Vue. Vue uses a build setup based on Vite, which we’ll configure to communicate with our server. Let’s get started.
Enter the Client Directory
First we’ll stop and exit the
server
directory and move into theclient
. Press^ + C
(control + C) in the terminal where the server is running then run the following command.# exit server directory and enter client cd ../client
Create a Vue Project
To create a Vue app, run the following command. This will create our project directly in the
client
folder as specified by the.
.npm create vue@latest .
You will be prompted to make several choices, such as package name and TypeScript support. For our project, name it
express-vue
and choose "yes" for TypeScript support, while selecting "no" for other optional features to keep the project simple.Next, install the dependencies and start the development server.
# Install dependencies npm install # Start the development server npm run dev
You should now have your Vue project running at http://localhost:5173/.
Add a
.env
fileAdd a
.env
in the root of theclient
folder with the following variables.VITE_SERVER_URL=http://localhost:3001 VITE_SERVER_API_PATH=/api
Please note that changes made to the
.env
file may require the server to be restarted. To stop the server, press^ + C
(control + C) in the terminal where the server is running. Then, to restart the server, runnpm run dev
again.Update
vite.config.ts
We want to customize our
vite.config.ts
file to enable communication between our client and server. ThedefineConfig
is a helper function that used to define configuration options in a Vite project. defineConfig can take either an object or a function as an argument. We’ll pass it a function so we can load our environment variables.```jsx import { fileURLToPath, URL } from 'node:url';
import vue from '@vitejs/plugin-vue'; import { defineConfig, loadEnv } from 'vite';
// https://vitejs.dev/config/ export default defineConfig((env) => { const envars = loadEnv(env.mode, './');
const serverURL = new URL( envars.VITE_SERVER_URL ?? 'http://localhost:3001' ); const serverAPIPath = envars.VITE_SERVER_API_PATH ?? '/api';
return { envDir: './',
// make the API path globally available in the client define: { API_PATH: JSON.stringify(serverAPIPath), },
plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), }, },
server: { port: 5173, proxy: { // proxy requests with the API path to the server // http://localhost:5173/api -> http://localhost:3001/api
}, }, }; });
Let's break down what each part of this configuration does.
* **Loading Environment Variables**: The `loadEnv` function is used to load environment variables based on the current mode (e.g., development, production). It reads the `.env` files in the project directory and loads variables into the `envars` object.
* **Parsing Server URL and API Path**: The server URL and API path are parsed and default values are provided if they are not found in the `.env` file.
* **Configuration Options**:
* `envDir`: Specifies the directory where environment variables are located.
* `define`: Allows defining global constants that will be replaced during the build process. In this case, `__API_PATH__` is defined with the value of the server API path.
* `plugins`: Specifies the Vite plugins used in the project. Here, the Vue.js plugin (`vue()`) is added.
* `resolve.alias`: Defines aliases for module resolution. In this case, the `@` alias is set to the `src`directory.
* **Server Configuration**:
* `server.port`: Specifies the port on which the Vite development server will run. Here, it's set to `5173`.
* `server.proxy`: The proxy settings enable communication between the client and the server. Requests matching the API path are forwarded to the server URL. When we send a request to `http://localhost:5173/api` it will be forwarded to our server at `http://localhost:3001/api`.
5. **Declaring Global Constant**
After adding a global constant in our `vite.config.ts` file, we need TypeScript to be aware of this constants for type checking. To achieve this, we declare the type definitions in the `env.d.ts` file.
```jsx
// client/env.d.ts
/// <reference types="vite/client" />
declare const __API_PATH__: string;
In this declaration, we inform TypeScript about the existence of the __API_PATH__
constant and specify its type as a string. This ensures that TypeScript provides type checking and IntelliSense support for this global constant throughout our project.
Sending Requests to the Server
Now that we've configured our Vite project to communicate with the server, let's update the
App.vue
file to send a request and display the response from the server. The following snippet demonstrates how to fetch data from the server using Vue.js composition API and display it in the app.// client/src/App.vue <script setup lang="ts"> import { ref } from "vue"; // Global constant containing the API base URL -> /api const baseURL = __API_PATH__; // Reactive variables for managing loading state and response message const isLoading = ref(false); const message = ref(""); // Function to fetch data from the server async function fetchAPI() { try { // Set loading state to true isLoading.value = true; // Send a GET request to the server const response = await fetch(baseURL); // Parse the JSON response const data = await response.json(); // Update the message with the response data message.value = data.message; } catch (error) { // Handle errors message.value = "Error fetching data"; console.error(error); } finally { // Reset loading state isLoading.value = false; } } </script> <template> <!-- Button to trigger the fetchAPI function --> <button @click="fetchAPI">Fetch</button> <!-- Display loading message while fetching data --> <p v-if="isLoading">Loading...</p> <!-- Display the response message if available --> <p v-else-if="message">{{ message }}</p> </template>
In this code snippet:
We import the
ref
function from Vue's composition API to create reactive variables for managing the loading state (isLoading
) and the response message (message
).The
fetchAPI
function is defined to send a GET request to the server using thefetch
API.While the request is being processed, the loading state is set to
true
, and a loading message is displayed.Once the request is complete, the loading state is reset, and the response message from the server is displayed.
Any errors that occur during the request are caught and
message
is updated.
With these updates, our Vue application is now capable of fetching data from the server and displaying it to the user.
Start both servers.
Since our client is already running, we need to start our server. If you're using VS Code, you can open a new terminal by right-clicking the Terminal option in the menu bar and selecting New Terminal. Alternatively, you can open a new terminal window and navigate to the project directory. Once you're in the project directory, start the server by running the following commands:
cd server npm run dev
This will navigate to the
server
directory and start the server in development mode.Initiating the Client-Side Request
The final step is to trigger the request from the client by clicking the button in
App.vue
. This will send a request to our sever.
Conclusion
Congratulations! 🎉 You've successfully learned how to set up a full-stack web development environment using Express.js and TypeScript for the backend and Vue.js with Vite for the frontend.
Throughout this guide, we've covered:
Setting up the Backend: We started by initializing an Express.js server with TypeScript, configuring middleware, handling routes, and compiling our code to be compatible for execution by Node.js.
Configuring the Frontend: Next, we configured Vite to allow communication between the client and server. We leveraged Vite’s server options to proxy specific requests to the server.
Client-Server Communication: With our environment set up, we learned how to send requests from the frontend to the backend using Vue.js composition API. By fetching data from the server and updating the UI, we created a responsive web application.
I hope this tutorial lessened your apprehensions towards working with server-side logic now that you've laid the foundation for building dynamic web applications. So keep experimenting, keep building, and don't hesitate to delve deeper into the full-stack side of things.
Happy coding!