Why? Explanation is here https://www.tiktok.com/@syntaxfm/video/7394478096591621406 host think of a host name for your local app for example local.webapp.com add the host to your dns code /etc/hosts 127.0.0.1 localhost 255.255.255.255 broadcasthost ::1 localhost 127.0.0.1 local.webapp.com or programmatically sudo echo "127.0.0.1 local.webapp.com" | sudo tee -a /etc/hosts Caddy proxy Install Caddy proxy server (similar to Nginx) https://caddyserver.com/docs/install Create caddyfile in the root # Dev server local.webapp.com { # Reverse proxy API requests handle /api/* { reverse_proxy http://localhost:4000 } # Reverse proxy all other requests to the frontend dev server handle /* { reverse_proxy http://localhost:3000 } } # Preview built react app local.webapp.com:4500 { # Reverse proxy API requests handle /api/* { reverse_proxy http://localhost:4000 } # Serve build static files handle_path /* { root * ./frontend/dist/ file_server try_files {path} /index.html } } Run caddy with caddy start Or integrate caddy launching into the npm script // package.json "scripts": { "start": "concurrently \"caddy start\" \"npm run start:db\" \"npm run start:backend\" \"npm run start:frontend\"", } Dev servers Front React Vite serves from port 3000 // vite.config.ts export default defineConfig({ server: { host: 'localhost', port: 3000, }, preview: { host: 'localhost', port: 4500, }, }) Backend serves from port 4000 // backend/index.ts const startWebServer = async (): Promise<void> => { try { const appRunnerHost = '0.0.0.0' const localHost = 'localhost' await fastify.listen({ port: 4000, host: developerMode ? localHost : appRunnerHost, }) fastify.log.info('Server listening on http://localhost:4000') // Start the background jobs after the web server has started. startBackgroundJobs() } catch (err) { fastify.log.error(err) process.exit(1) } } Requests to backend All requests to backend go to https://local.webapp.com/api import axios from 'axios' export const axiosInstance = axios.create({ baseURL: 'https://local.webapp.com/api', withCredentials: true, }) SSL And boom, we have a true ssl. Vite proxy with ssl We can use internal vite's proxy and wire all requests and responses with backend through it It can accept some ssl from basicSsl package import basicSsl from '@vitejs/plugin-basic-ssl' import react from '@vitejs/plugin-react' import { defineConfig } from 'vite' import tsconfigPaths from 'vite-tsconfig-paths' export default defineConfig(({ command, mode }) => { return { root: './front/', server: { host: 'localhost', port: 3000, proxy: { '/api': 'http://localhost:4000', }, }, plugins: [react(), tsconfigPaths(), basicSsl()], } }) Requests from backend are made to /api prefixed endpoints import type { ResBody, ReqBody as Payload } from '@back/api/auth/registerRouter' import { useMutation, type UseMutationResult } from '@tanstack/react-query' import axios, { type AxiosError } from 'axios' import { queryKey } from '@shared/consts/queryKey' export const useRegisterMutation = (): UseMutationResult< ResBody, AxiosError<ResBody>, Payload > => { const query = useMutation<ResBody, AxiosError<ResBody>, Payload>({ mutationKey: [queryKey.register], mutationFn: async ({ email, password }: Payload) => { const res = await axios<ResBody>({ url: '/api/register', method: 'post', data: { email, password }, }) return res.data }, }) return query } import 'dotenv/config' import cookieParser from 'cookie-parser' import express from 'express' import { registerRouter } from './api/auth/registerRouter' import { errorHandlerMiddleware } from './middleware/errorHandlerMiddleware' import type { Req, Res } from './types' const app = express() app.use(express.json({ limit: '50mb' })) app.use(cookieParser()) app.get('/api', (_req: Req, res: Res) => res.send('i am express.js')) app.use('/api/register', registerRouter) app.use(errorHandlerMiddleware) app.listen(4000, () => { console.info(`🚀 Started at http://localhost:4000`) }) No proxy If you do not use external or internal vite's proxy you have manage ssl certificate manually To be continued...