Setting up an Express + Typescript Server with Vue + Vite

Setting up an Express + Typescript Server with Vue + Vite

A Beginner’s Guide to Full-Stack Development

·

14 min read


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.

  1. 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
    
  2. Navigate to the Project Directory.

    Once the directory is created, navigate into it using the cd command.

     cd express-starter
    
  3. Create Server and Client Folders.

    Once inside express-starter directory, create two folders: client and server then navigate into the server folder.

     mkdir server client
     cd server
    
  4. Initialize a New Node.js Project.

    Use npm init to initialize a new Node.js project. This will generate a package.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
    
  5. 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.

  1. Generate a tsconfig.json

    Create a tsconfig.json file to configure TypeScript. This file specifies how TypeScript should compile our code. Run npx tsconfig.json then select Node. This command generates a tsconfig.json file with some default settings.

     npx tsconfig.json
    
  2. Create Source Files and Update package.json

    Next, create a src directory. Within the src 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 use main.js instead of index.js .

     "main": "main.js",
    
  3. 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 the server folder.

     touch .env
    
  4. 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 the server folder. For now we’ll add node_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.

  1. 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:

  1. 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!".

  1. 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 the src 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 runs tsc to compile TypeScript files and then executes node ./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.

  1. Enter the Client Directory

    First we’ll stop and exit the server directory and move into the client. 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
    
  2. 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/.

  3. Add a .env file

    Add a .env in the root of the client 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, run npm run dev again.

  4. Update vite.config.ts

    We want to customize our vite.config.ts file to enable communication between our client and server. The defineConfig 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.

  1. 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 the fetch 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.

  1. 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.

  2. 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!