Setting up a full-stack application with Node, Express, Vite, React, tailwindcss, twin.macro and styled-components

Setting up a full-stack application with Node, Express, Vite, React, tailwindcss, twin.macro and styled-components

For small to medium size applications

The Stack 🚀

  • vite: a fast, lightweight frontend build tool that uses ES modules and service workers to provide a smooth development experience.

  • tailwindcss: a utility-first CSS framework for rapidly building custom user interfaces.

  • twin.macro: a Babel macro that enables users to write CSS in JavaScript using a Tagged Template Literal syntax similar to styled-components.

  • styled-components: a library for creating and styling reusable React components using a familiar CSS-in-JS syntax.

  • node.js: an open-source, cross-platform JavaScript runtime environment that executes JavaScript code outside of a web browser.

  • express: a minimalist web framework for Node.js that provides a set of features for building web applications and APIs.

🤔 Why vite over create-react-app?

  1. Vite is built around the concept of a modern JavaScript bundler called esbuild, which is designed to be fast and lightweight. This can result in faster builds and rebuilds when compared to create-react-app, which uses webpack.

  2. Vite includes a built-in development server that utilizes native ES module imports and service workers to provide a smooth development experience with hot module reloading (HMR).

  3. Vite has a simple configuration file (vite.config.js) that is easy to understand and customize.

  4. Vite is designed to be flexible and extensible, and it provides a plugin system that allows users to customize the build process or add additional features.

  5. Vite has a smaller footprint than create-react-app, as it does not include a lot of the boilerplate and configuration that comes with create-react-app by default. This can be a benefit for developers who want a simpler, more lightweight tool.

🤔 Why twin.macro, tailwindcss, and styled-components for styling?

  1. twin.macro allows you to use the syntax and features of styled-components to define your styles, while still taking advantage of the utility-first approach of tailwindcss.

  2. The combination of twin.macro and tailwindcss can make it easier to build custom user interfaces quickly, as you can use the predefined classes provided by tailwindcss to style your components, and use twin.macro to override or extend those styles as needed.

  3. Using styled-components with twin.macro and tailwindcss can help to keep your component-level styles organized and maintainable, as you can define all of the styles for a component in a single place.

  4. twin.macro can help to improve the performance of your application by automatically purging unused styles from your CSS, reducing the size of your stylesheets and improving the loading time of your application.

  5. The combination of twin.macro, tailwindcss, and styled-components can provide a powerful and flexible toolset for styling your React applications, enabling you to build custom user interfaces with ease.

🤔 Why node.js and express?

  1. Node.js is built on top of the V8 JavaScript runtime, which makes it efficient and fast for building scalable network applications.

  2. Node.js has a large and active developer community, which means there are many resources available for learning and troubleshooting.

  3. Express is a minimalist web framework for Node.js that provides a set of features for building web applications and APIs. It is easy to learn and use, and it has a small footprint, making it well-suited for intermediate-level full-stack applications.

  4. Express is built on top of the middleware concept, which makes it easy to extend and customize the functionality of your application.

  5. Node.js and Express are well-suited for building real-time applications, such as chat applications or collaborative tools, as they can handle a high number of concurrent connections and provide low latency.

  6. Both Node.js and Express have a large ecosystem of third-party libraries and plugins that can be easily integrated into your application, allowing you to build a wide range of functionality with minimal effort.

Now that we have set the stack and reasons for using the specifics out of the way let's dive right into the setup process of our application.

I will be using:

  • Visual Studio Code (vscode) for this demonstration as it is my favourite of all other code editors and I am very comfortable using it. You can any code editor of your choice.

  • Node Package Manager (NPM) as my default package manager, You can also use yarn .

⚙️ The Setup

Step 1️⃣ : Create a project, Initialise/setup the server using npm and segregate the backend (server) and frontend (client) responsibilities

  • create a new folder and open it inside vscode

  • open the terminal in vscode using command + j on mac and ctrl + j on windows

  • run npm init -y in the terminal

npm init -y is a command that is used to create a new package.json file for a Node.js project. The -y flag tells npm to use the default values for the package.json fields, so you don't have to answer any prompts.

  • you will a new file package.json created in the root of your project looking something like this

      {
        "name": "fullstackdemo",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
          "test": "echo \"Error: no test specified\" && exit 1"
        },
        "keywords": [],
        "author": "",
        "license": "ISC"
      }
    
  • once the package.json has been initialised, we need to create a new .js file in the root that will contain our main server logic. I will be naming it index.js

  • next, create a new folder in the root of your project (next to the above created package.json) I am naming it client. This will contain all our code related to frontend that we will be creating using vite, shortly.

  • now, we need to install the necessary dependencies on our server-side. in your terminal, run:

       npm install concurrently express nodemon
    

    This will create a node_modules folder where all the dependencies for the server will be saved. We can also notice that the package.json contents have now been updated.

    while we are at our server's package.json let's also update the scripts that will run our server and client concurrently. We are using nodemon to automatically rerun our server whenever we make any changes to our code.

        "scripts": {
          "server": "nodemon index.js",
          "client": "npm run dev --prefix client",
          "dev": "concurrently \"npm run server\" \"npm run client\""
        },
    

    and this is how the package.json should be looking at this point:

      {
        "name": "fullstackdemo",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
          "server": "nodemon index.js",
          "client": "npm run dev --prefix client",
          "dev": "concurrently \"npm run server\" \"npm run client\""
        },
        "keywords": [],
        "author": "",
        "license": "ISC",
        "dependencies": {
          "concurrently": "^7.6.0",
          "express": "^4.18.2",
          "nodemon": "^2.0.20"
        }
      }
    

Step 2️⃣ : Initialize the React application inside client folder using vite and configure vite.config.js to proxy any API requests to the server

  • Inside your client folder run:

      npm create vite@latest
    

    and complete the prompt by giving the inputs asked. Enter . for your project name so that the project is initialised in the client folder itself. Choose react when asked to select a framework and javascript as the variant for this demo.

    The folder structure should be looking like this on successful initialisation.

  • next, install all the dependencies that are needed for setting up our react client.

      npm install @emotion/core @emotion/react @emotion/styled styled-components twin.macro vite-plugin-babel-macros
    
  • once we have these dependencies installed, we can proceed to set up tailwindcss in our react application

    Follow the official tailwind guide to setup with vite here: https://tailwindcss.com/docs/guides/vite

    Note: since we have already setup our client folder, we can follow the documentation step 2 onwards.

  • after completing the tailwindcss setup you will have two new files in your client folder, tailwind.config.cjs and postcss.config.cjs

  • next, we need to configure vite. This will be done by adding rules to vite.config.js

      import { defineConfig } from 'vite';
      import react from '@vitejs/plugin-react';
      import macrosPlugin from 'vite-plugin-babel-macros';
    
      // https://vitejs.dev/config/
      export default defineConfig({
        plugins: [macrosPlugin(), react()],
        server: {
          port: 3000,
          proxy: {
            '/api': {
              target: 'http://localhost:6001',
              changeOrigin: true,
              secure: false,
              ws: true,
            },
          },
        },
      });
    

    ❗️plugins:

    vite-plugin-babel-macros is a Vite plugin that allows you to use advanced code transformations (macros) at compile time, in React projects, without a complex build system. It uses babel-plugin-macros and allows you to import macros from libraries like babel-plugin-react-css-modules to transform CSS modules into CSS classes at compile time, streamlining the development process for React projects.

    ❗️server:

    • The port property is set to 3000, which means that the development server will run on port 3000. This means that when you start the development server, you'll be able to access your application in your web browser at http://localhost:3000.

    • The proxy property is set to an object that defines a single proxy route. The proxy is used to forward certain requests from the development server to another server or service.

      • The route '/api' is defined, this is a path that when the development server receives a request for it, will forward the request to the target specified.

      • target property is set to http://localhost:6001, This is the URL of the target server. All requests to /api will be forwarded to this URL.

      • changeOrigin: true change the request's origin from localhost to the target origin.

      • secure: false this means that self-signed SSL certificates will be accepted.

      • ws: true WebSocket proxy, If you're proxying WebSocket connections, this property should be set to true.

    This setup allows you to run the frontend and the backend on different ports and different servers but the frontend can make the requests to the backend as if they were on the same origin.

    This means that requests to http://localhost:3000/api/* will be forwarded to http://localhost:6001/* allowing your front-end to interact with your back-end services as if they were running on the same server.

    It's worth noting that the secure: false and ws: true are optional, You can leave it out or set it to false if you don't want it.

Step 3️⃣ : Write server code and start the development environment

We will be creating a simple API endpoint to fullfill a simple GET request that returns some static data sufficient for this demonstration.

  • inside index.js of your root directory, add the following code:

      const express = require('express');
      const app = express();
    
      const ENV = 'development';
      const DOMAIN = ENV === 'development' ? 'localhost' : '';
      const PORT = 6001;
    
      app.use(express.json({ extended: false }));
    
      app.get('/api', (req, res) => {
        try {
          const mockData = {
            firstName: 'Adeesh',
            lastName: 'Sharma'
          };
    
          res.json(mockData);
        } catch (err) {
          res.status(500).json({error: err.message });
        }
      });
    
      app.listen(PORT, `${DOMAIN}`, () => {
        console.log(`Server listening on port ${PORT}`);
      });
    
  • now, fire up the dev server by running the following command in the root folder

      npm run dev
    

  • test the react application by navigating to localhost:3000 in the browser

  • test the API endpoint by hitting localhost:3000/api

🎉 We have successfully set up a full-stack application running express in the backend and serving a react client 🎉

Step 4️⃣: Create a simple Application page in react using tailwindcss and refactor with twin.macro

  • contents of client/src/App.js

      import React, { useState, useEffect } from 'react';
      import './index.css';
    
      const App = () => {
        const [data, setData] = useState(null);
    
        useEffect(() => {
          const apiCall = async () => {
            const response = await fetch('/api');
            const data = await response.json();
            setData(data);
          };
    
          apiCall();
        }, []);
    
        return (
          <div className='bg-indigo-200 h-screen'>
            <div className='max-w-7xl mx-auto py-16 px-4 sm:py-24 sm:px-6 lg:px-8'>
              <div className='text-center'>
                <h2 className='text-base font-semibold text-indigo-600 tracking-wide uppercase'>
                  Demonstration
                </h2>
                <p className='text-red-600 mt-1 text-4xl font-extrabold sm:text-5xl sm:tracking-tight lg:text-6xl'>
                  Full-stack application
                </p>
                {data && (
                  <p className='max-w-xl mt-5 mx-auto text-xl text-gray-500'>
                    with ♥️ by {data.firstName} {data.lastName}
                  </p>
                )}
              </div>
            </div>
          </div>
        );
      };
    
      export default App;
    
  • refactor with twin.macro and styled-components

      import React, { useState, useEffect } from 'react';
      import './index.css';
      import styled from 'styled-components';
      import tw from 'twin.macro';
      import vite from '/vite.svg';
    
      const App = () => {
        const [data, setData] = useState(null);
    
        useEffect(() => {
          const apiCall = async () => {
            const response = await fetch('/api');
            const data = await response.json();
            setData(data);
          };
    
          apiCall();
        }, []);
    
        return (
          <ApplicationContainer>
            <SectionContainer>
              <Image src={vite} />
              <CenteredText>
                <H2>Demonstration</H2>
                <Title>Full-stack application</Title>
                {data && (
                  <Author>
                    with ♥️ by {data.firstName} {data.lastName}
                  </Author>
                )}
              </CenteredText>
            </SectionContainer>
          </ApplicationContainer>
        );
      };
    
      export default App;
    
      const ApplicationContainer = tw.div`bg-indigo-200 h-screen`;
      const SectionContainer = tw.div`max-w-7xl mx-auto py-16 px-4 sm:py-24 sm:px-6 lg:px-8`;
    
      const CenteredText = styled.div`
        ${tw`text-center`}
      `;
    
      const H2 = styled.h2`
        font-weight: 800;
        ${tw`text-base text-indigo-600 tracking-wide uppercase`}
      `;
    
      const Title = tw.p`text-red-600 mt-1 text-4xl font-extrabold sm:text-5xl sm:tracking-tight lg:text-6xl`;
      const Author = tw.p`max-w-xl mt-5 mx-auto text-xl text-gray-500`;
      const Image = styled.img`
        ${tw`h-40 flex justify-center w-full`}
      `;
    
  • Final Product 👏🏻

🔗 GitHub repository

Link: https://github.com/adeeshsharma/fullstack-setup

💭 Conclusion

A development setup that uses Node, Express, Vite, React, Tailwind CSS, twin.macro, and styled-components is an excellent choice for building modern web applications. Each of these technologies offers powerful and flexible features that can help to reduce development time and streamline the process of building web applications.

Node and Express provide a robust and scalable foundation for building back-end services, while Vite offers a fast and easy-to-use development server that allows you to quickly test and iterate on your front-end code. React, on the other hand, is a popular and widely used JavaScript library that makes it easy to build reusable UI components, while Tailwind CSS is a utility-first CSS framework that can help to speed up the process of styling your application.

twin.macro and styled-components can further reduce development time by allowing you to use advanced code transformations at compile time, without having to use a complex build system. twin.macro which is a library that allows you to use macros in your React code and styled-components is a library that allows you to write CSS styles in JavaScript, and it can be used to style React components.

In conclusion, this setup is a powerful and efficient way to build web applications, thanks to the simplicity and flexibility provided by Node, Express, Vite, React, Tailwind CSS, twin.macro and styled-components. It can save you a lot of development time, compared to other frameworks and libraries, by simplifying the process of building modern web applications.

Did you find this article valuable?

Support Adeesh's Software Engineering Insights by becoming a sponsor. Any amount is appreciated!