Search code examples
node.jstypescriptexpresses6-modulesdotenv

express - config file gets called called before app or server files which returns env variable to be undefined


in my app the server.ts is the entrypoint which serves the app in port. all app.use is done in the app.ts file where the dotenv is called. As far as I know you are only supposed to call dotenv once and the entire app should have the variables. But for some reason the config file gets loaded first before the app or server files which is when the env variable process.env.WHITELIST_ORIGINS starts with undefined. I cannot understand the reason why this is happening. Is there any way to fix this without having to call dotenv again from the config file

server.ts

import http from 'http'
import { Socket } from 'socket.io'
import app from './app'
import { setIO } from './config'

const server = http.createServer(app)

const io = setIO(server)

io.on('connection', function (socket: Socket) {
  console.log('New client connected with id => ', socket.id)

  socket.on('disconnect', function (reason): void {
    console.log('A client disconnected with id => ', socket.id, ' reason => ', reason)
  })
})

const port = process.env.PORT || 8095

server.listen(port, () => {
  console.log(`App running`)

})

console.log('server call')

app.ts

import compression from 'compression'
import cors from 'cors'
import dotenv from 'dotenv'
import express, { Application } from 'express'
import helmet from 'helmet'
import { errorHandler, verifyToken } from './middleware'
import router from './router'

dotenv.config()

const app: Application = express()

app.use(compression())
app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(helmet())
app.use(verifyToken)
app.use('/', router)
app.use(errorHandler)

console.log('app call')

export default app

config/socket-config.ts

import { Server as HttpServer } from 'http'
import { Server } from 'socket.io'
// import dotenv from 'dotenv'
// dotenv.config() - If this is called here it works fine.

let _io: Server

const whitelistOrigins = process.env.WHITELIST_ORIGINS
console.log("whitelistOrigins", whitelistOrigins) // This logs undefined

console.log('whitelistOrigins', whitelistOrigins)

export const setIO = (server: HttpServer): Server => {
  _io = new Server(server, {
    cors: {
      origin: whitelistOrigins,
      methods: ['GET', 'POST'],
      credentials: true,
    },
    allowEIO3: true,
  })
  return _io
}

export const getIO = () => {
  return _io
}

console.log('socket call')

Here is the order of how console logs

whitelistOrigins undefined
socket call
app call
server call
App running

Solution

  • The issue is described in detail within the dotenv docs

    When you run a module containing an import declaration, the modules it imports are loaded first, then each module body is executed in a depth-first traversal of the dependency graph, avoiding cycles by skipping anything already executed.

    For this reason, dotenv provides a specific import that executes config() within its body.

    import 'dotenv/config';
    

    It's best to add this as close to your app's entry-point as possible. I would place this first in server.ts