Chapter 8 - Manage the environment variables with dotenv¶
Applications often run in different environments. Depending on the environment, different configuration settings should be used. We need to have a way to easily update the configuration without having to change the codebase. A common way to do so is to use environment variables. These are variables that are stored and loading from your operating system.
dotenv offers the possibility to have a .env file (pronounced "dot env") from where it will load the defined variables as environment variables in your application.
In this chapter, you will manage environment variables in your frontend and backend services. You'll update your code so it uses environmental variables with the help of dotenv to load these from .env files.
This will allow to easily update the listening port of the backend or the color of the stroke and the backend URL of the frontend and be conform to The Twelve-Factor App methodology.
At the moment, the port of the backend service is fixed to 4000. In some cases, you might want to change this port because another process already uses that port.
NestJS offers a package to load environment variables.
It is standard practice to throw an exception during application startup if required environment variables haven't been provided or if they don't meet certain validation rules. With Joi, you define an object schema and validate JavaScript objects against it.
For example, a port has to be a number. The validation schema can check if the port is a valid number or not, and warn the user if it's not. By default, all environment variables are treated as strings.
The following code could be put directly in the src/app.module.ts file. However, it is cleaner to keep things separated and create a new config package with its own module that can then be loaded by the main file.
Inject the Config service in the WebSocket gateway¶
In order to illustrate the usage of the Config module with NestJS dependency injection, update the WebSocket gateway to access the configuration and log a message on module initialization.
import{OnModuleInit}from"@nestjs/common";import{ConfigService}from"@nestjs/config";import{ConnectedSocket,MessageBody,OnGatewayConnection,OnGatewayDisconnect,SubscribeMessage,WebSocketGateway,}from"@nestjs/websockets";import{Socket}from"socket.io";import{Point}from"./types/point.type";@WebSocketGateway({cors:{origin:"*",},})exportclassAppGatewayimplementsOnModuleInit/* (1)! */,OnGatewayConnection,OnGatewayDisconnect{constructor(readonlyconfigService:ConfigService/* (2)! */){}onModuleInit(){// (3)!constport=this.configService.get<number>("PORT");// (4)!console.log(`The WebSocket gateway runs on port ${port}.`);// (5)!}handleConnection(@ConnectedSocket()socket:Socket):void{console.log("A player has connected");}handleDisconnect(@ConnectedSocket()socket:Socket):void{console.log("A player has disconnected");}@SubscribeMessage("FIRST_POINT_FROM_PLAYER")start(@ConnectedSocket()socket:Socket,@MessageBody()point:Point):void{socket.broadcast.emit("FIRST_POINT_TO_PLAYERS",point);}@SubscribeMessage("POINT_FROM_PLAYER")addPoint(@ConnectedSocket()socket:Socket,@MessageBody()point:Point):void{socket.broadcast.emit("POINT_TO_PLAYERS",point);}}
The OnModuleInit interface force the class to implement the onModuleInit method.
The ConfigService service is injected and available within the class.
The concret implementation of the onModuleInit method from the OnModuleInit interface. This method will be called when the AppGateway is fully initialized.
The PORT environment variable is accessed from the Config service.
Dependency injection cannot be used in the main file as it was done with the WebSocket gateway. The ConfigService has to be taken from the application that has been initialized and then it can use it.
Get the ConfigService from the initialized application.
Access to the PORT environment variable. As environment variables are strings by default, we specify the config service the PORT environment variable is a number with get<number>('PORT').
However, the config service might not have the PORT environment variable set and would be undefined. As we use Joi, we know the variable will always be defined, so we force the cast with as number.
Try out the backend and manually set the environment variables¶
You notice the port of the WebSocket gateway now runs on the port 1234 that you defined with an environment variable!
An environment variable can be set by setting it in front of the command you want to execute. You'll later see other ways to define environment variable.
Stop the application by pressing Ctrl+C in your terminal.
Store the environment variable in a dedicated .env file¶
The PORT=1234 npm run start:dev command can be useful to set one specific environment variable but is not very user-friendly for multiple environment variables or to share with other people.
dotenv allows to set these environment variables in a dedicated "dot env" (.env) file. It will then take the values from the .env file, load them as environment variables in a similar matter that you did with the PORT=1234 npm run start:dev command.
Store the value of the port in a dedicated .env file.
At the moment, the backend URL used by the frontend service is fixed to localhost:4000. To be able to connect to other backends on the internet or on other devices, this value must be changed.
Next.js uses dotenv internally as well (so there is no need to install it) but the process defers a bit from NestJS.
In Next.js, runtime environment variables can only be accessed in Next.js Pages (in the src/pages directory). Thus, the pages must provider the environment variables as properties to the React components that need them.
This is because the default process of building React application sets the environment variables at build time. When building your application for production, React will read the values set in environment variables and substitute them in the codebase.
Next.js offers the server side rendering feature that we can use to build dynamic pages where each page can still have access to the real environment variables.
import{createContext,useContext,useEffect,useMemo}from"react";import{Socket,io}from"socket.io-client";import{useLines}from"@/components/line-provider";import{Point}from"@/types/point";typeState={emitFirstPointFromPlayer:Function;emitPointFromPlayer:Function;};constWebSocketContext=createContext<State|undefined>(undefined);exporttypeWebSocketProviderProps={// (1)!backendUrl:string;children:React.ReactNode;};functionWebSocketProvider({backendUrl,children}:WebSocketProviderProps/* (2)! */):JSX.Element{constsocket:Socket=useMemo(()=>io(backendUrl/* (3)! */,{autoConnect:false}),[backendUrl]// (4)!);const{dispatch}=useLines();useEffect(()=>{if(!socket.connected){socket.connect();}return()=>{socket.close();};},[socket]);useEffect(()=>{if(!socket)return;socket.on("FIRST_POINT_TO_PLAYERS",(point:Point)=>{dispatch({type:"ADD_FIRST_POINT",point:point});});socket.on("POINT_TO_PLAYERS",(point:Point)=>{dispatch({type:"ADD_POINT",point:point});});},[socket,dispatch]);constemitFirstPointFromPlayer=(point:Point)=>{socket.emit("FIRST_POINT_FROM_PLAYER",point);};constemitPointFromPlayer=(point:Point)=>{socket.emit("POINT_FROM_PLAYER",point);};return(<WebSocketContext.Providervalue={{emitFirstPointFromPlayer,emitPointFromPlayer}}>{children}</WebSocketContext.Provider>);}functionuseWebSocket():State{constcontext=useContext(WebSocketContext);if(context===undefined){thrownewError("useWebSocket must be used within a WebSocketProvider");}returncontext;}export{WebSocketProvider,useWebSocket};
Define a type for the WebSocket provider to add a backendUrl property.
Access the properties available to the WebSocket provider.
Use the property for the backend URL.
Add the backendUrl as a dependency of the useMemo function. If the variable backendUrl changes, the useMemo function is called again.
Update the main page to pass the backend URL from the environment variables. The process.env variable contains all the environment variable available to the process.
Define the properties the Home component can have access to.
The getServerSideProps function will be called each time the main page is accessed. It will retrieve the properties of the page from the environment variables and pass them to the React component.
Try to use the BACKEND_URL environment variable. If it is not set, set the value to localhost:4000.
The backendUrl is passed from the getServerSideProps function.
The backendUrl is then passed to your WebSocket provider.
Try out the frontend and manually set the environment variables¶
Update the dotenv file of the backend to set the port back to 4000. Even if there is a default value set with Joi, it is good practice to set all environment variables in the .env file for a quick overview of the available environment variables.
Congrats! You now have a simple way to update the configuration of your application using environment variables!
There is no need to access and modify the codebase to change trivial settings.
Using environment variables is one of the best practices as mentioned by The Twelve-Factor App methodology. It allows to update the application in different settings without the need of rebuilding the entire application each time. Every time you implement something new, don't forget to search the subject with best practices/proven practices on your favorite search engine, you'll learn so much!
The next chapter, you'll use these environment variables to access your drawing application from your phone! The friends on your network will be able to access it as well and you can draw collaboratively.
Are you able make usage of environment variables for the stroke color and the stroke size on the frontend side? Expand the next component to see the answer!
Show me the answer!
Update the Sketch to get the stroke color and the stroke size as properties