DEV Community

karoon sillapapan
karoon sillapapan

Posted on

TypeORM Timezone Issue in React + NestJS Stack

πŸ“ Problem Summary

Environment Setup

  • Frontend (React + Vite): No timezone configuration β†’ Uses Browser Default (UTC+7)
  • Backend (NestJS + TypeORM): No timezone configuration
  • Database: Configured as UTC+0
  • Server Environment: UTC+7 (Thailand timezone)

Symptoms

  1. Frontend: Creates transaction at 2025-06-16 13:52 (UTC+7)
  2. Backend receives: 2025-06-16 06:52 (UTC) βœ… Correct
  3. Database stores: 2025-06-16 06:52 (UTC) βœ… Correct
  4. Backend reads from Database: 2025-06-15T23:52 ❌ Wrong by 7 hours!
  5. Swagger displays: 2025-06-15 23:52 ❌ Wrong!

🌐 Frontend Timezone Behavior

React/JavaScript Without Timezone Config = Uses Browser Default

Timezone Source in Browser

// JavaScript gets timezone from Browser's Local Timezone
console.log(Intl.DateTimeFormat().resolvedOptions().timeZone);
// Result: "Asia/Bangkok" (if in Thailand)

console.log(new Date().getTimezoneOffset());
// Result: -420 (minutes) = -7 hours (UTC+7)
Enter fullscreen mode Exit fullscreen mode

Timezone Source Priority Order

  1. Operating System (Windows/Mac/Linux timezone setting)
  2. Browser setting (sometimes can override)
  3. Location detection (if browser allows)

Date Object Behavior in Frontend

// When user creates transaction in Thailand (UTC+7)
const now = new Date(); // Current time

console.log(now.toString());
// "Mon Jun 16 2025 13:52:00 GMT+0700 (Indochina Time)"

console.log(now.toLocaleString());
// "16/6/2025, 13:52:00" (displays as local time)

console.log(now.toISOString());
// "2025-06-16T06:52:00.000Z" (converts to UTC for Backend)
Enter fullscreen mode Exit fullscreen mode

Sending Data to Backend

// React sends data
fetch('/api/transactions', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    amount: 1000,
    createdAt: new Date() // JavaScript auto-converts to ISO string
  })
});

// JSON.stringify automatically calls toISOString()
// Sends as: "2025-06-16T06:52:00.000Z" βœ… Correct!
Enter fullscreen mode Exit fullscreen mode

Testing Frontend Timezone

Add this Component to Check Timezone Info

// TimezoneDebug.jsx
import { useEffect, useState } from 'react';

function TimezoneDebug() {
  const [timezoneInfo, setTimezoneInfo] = useState({});

  useEffect(() => {
    const now = new Date();
    setTimezoneInfo({
      timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
      offset: now.getTimezoneOffset() / -60,
      localTime: now.toLocaleString(),
      isoTime: now.toISOString(),
      utcTime: now.toUTCString()
    });
  }, []);

  return (
    <div style={{ padding: '16px', border: '1px solid #ccc', margin: '8px' }}>
      <h3>Browser Timezone Info</h3>
      <p><strong>Timezone:</strong> {timezoneInfo.timezone}</p>
      <p><strong>UTC Offset:</strong> +{timezoneInfo.offset} hours</p>
      <p><strong>Local Time:</strong> {timezoneInfo.localTime}</p>
      <p><strong>ISO Time (sent to Backend):</strong> {timezoneInfo.isoTime}</p>
      <p><strong>UTC Time:</strong> {timezoneInfo.utcTime}</p>
    </div>
  );
}

export default TimezoneDebug;
Enter fullscreen mode Exit fullscreen mode

Expected Results (in Thailand)

Timezone: Asia/Bangkok
UTC Offset: +7 hours
Local Time: 16/6/2025, 13:52:00
ISO Time (sent to Backend): 2025-06-16T06:52:00.000Z
UTC Time: Mon, 16 Jun 2025 06:52:00 GMT
Enter fullscreen mode Exit fullscreen mode

Displaying Time from Backend

// Display time from Backend correctly in User's timezone
function TransactionTime({ utcTimeFromBackend }) {
  // Backend sends: "2025-06-16T06:52:00.000Z"
  // Display as: "16/6/2025, 13:52:00" (local time)

  const localTime = new Date(utcTimeFromBackend).toLocaleString('en-US', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  });

  return <span>{localTime}</span>;
}
Enter fullscreen mode Exit fullscreen mode

Frontend Behavior Summary

Frontend is working correctly! βœ…

  • User sees: 13:52 (local time UTC+7)
  • Sends to Backend: 06:52 UTC (auto-converted)
  • This behavior is standard and correct

The problem is in Backend (TypeORM), not Frontend!


πŸ” Root Cause Analysis

TypeORM Timezone Conversion Process

1. Main Issue: TypeORM Doesn't Know Database Timezone

// When no timezone config in TypeORM
TypeOrmModule.forRoot({
  type: 'mysql',
  // Missing timezone config ← Problem is here!
})
Enter fullscreen mode Exit fullscreen mode

2. What Happens Inside TypeORM

// Database returns: "2025-06-16 06:52:00" (no timezone info)
// TypeORM thinks: "This is time in Server Local Timezone (UTC+7)"
// TypeORM converts: 06:52 UTC+7 β†’ 23:52 UTC (previous day)
// Result: "2025-06-15T23:52" ❌
Enter fullscreen mode Exit fullscreen mode

3. Wrong Conversion Process

Database (UTC+0):     2025-06-16 06:52:00
       ↓
TypeORM assumes:      2025-06-16 06:52:00 (UTC+7) ← Wrong interpretation!
       ↓  
Converts to UTC:      2025-06-16 06:52 - 7 hours = 2025-06-15 23:52
       ↓
Final result:         2025-06-15T23:52 ❌
Enter fullscreen mode Exit fullscreen mode

βœ… Solutions

1. Fix TypeORM Database Configuration

Method 1: In app.module.ts

// app.module.ts
TypeOrmModule.forRoot({
  type: 'mysql', // or postgres
  host: 'your-host',
  port: 3306,
  username: 'username',
  password: 'password',
  database: 'database_name',

  // ⭐ Critical: Tell TypeORM that database uses UTC
  timezone: 'Z', // or '+00:00'

  // Additional settings
  dateStrings: false, // Return Date objects instead of strings
  entities: [__dirname + '/**/*.entity{.ts,.js}'],
  synchronize: false,
})
Enter fullscreen mode Exit fullscreen mode

Method 2: In ormconfig.json

{
  "type": "mysql",
  "host": "your-host",
  "port": 3306,
  "username": "username",
  "password": "password",
  "database": "database_name",
  "timezone": "Z",
  "dateStrings": false,
  "entities": ["dist/**/*.entity{.ts,.js}"],
  "synchronize": false
}
Enter fullscreen mode Exit fullscreen mode

Method 3: Using Environment Variables

// app.module.ts
TypeOrmModule.forRoot({
  type: process.env.DB_TYPE as any,
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT),
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  timezone: 'Z', // ⭐ Add this line
  entities: [__dirname + '/**/*.entity{.ts,.js}'],
})
Enter fullscreen mode Exit fullscreen mode

2. Set Server Timezone (Additional)

In main.ts

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

// ⭐ Force Node.js to use UTC
process.env.TZ = 'UTC';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Debug timezone (remove after fixing)
  console.log('Server timezone:', Intl.DateTimeFormat().resolvedOptions().timeZone);
  console.log('Process TZ:', process.env.TZ);

  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

3. Fix in Entity (For Additional Control)

// transaction.entity.ts
import { Entity, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';

@Entity('transactions')
export class Transaction {
  @Column({ type: 'int', primary: true, generated: true })
  id: number;

  // ⭐ Force column to be UTC
  @CreateDateColumn({ 
    type: 'timestamp',
    transformer: {
      to: (value: Date) => value,
      from: (value: string | Date) => {
        // Force database data to be interpreted as UTC
        if (typeof value === 'string') {
          return new Date(value + (value.includes('Z') ? '' : 'Z'));
        }
        return value;
      }
    }
  })
  createdAt: Date;

  @UpdateDateColumn({ 
    type: 'timestamp',
    transformer: {
      to: (value: Date) => value,
      from: (value: string | Date) => {
        if (typeof value === 'string') {
          return new Date(value + (value.includes('Z') ? '' : 'Z'));
        }
        return value;
      }
    }
  })
  updatedAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Testing and Debugging

1. Create Debug Endpoint

// In controller
@Get('debug-timezone')
async debugTimezone() {
  // Test raw query
  const rawQuery = await this.transactionRepository.query(
    'SELECT created_at FROM transactions ORDER BY id DESC LIMIT 1'
  );

  // Test TypeORM query
  const typeormQuery = await this.transactionRepository.findOne({
    order: { id: 'DESC' }
  });

  return {
    rawFromDatabase: rawQuery[0]?.created_at,
    fromTypeORM: typeormQuery?.createdAt,
    typeormISO: typeormQuery?.createdAt?.toISOString(),
    serverTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
    processEnvTZ: process.env.TZ
  };
}
Enter fullscreen mode Exit fullscreen mode

2. Expected Results After Fix

{
  "rawFromDatabase": "2025-06-16 06:52:00",      // Direct from database
  "fromTypeORM": "2025-06-16T06:52:00.000Z",     // Through TypeORM (with Z)
  "typeormISO": "2025-06-16T06:52:00.000Z",      // Converted to ISO string
  "serverTimezone": "UTC",                       // Server timezone
  "processEnvTZ": "UTC"                          // Process timezone
}
Enter fullscreen mode Exit fullscreen mode

πŸ“‹ Fix Checklist

βœ… What to Do

  • [ ] Add timezone: 'Z' to TypeORM config
  • [ ] Set process.env.TZ = 'UTC' in main.ts
  • [ ] Test with debug endpoint
  • [ ] Verify Swagger displays correct time

⚠️ What to Avoid

  • Don't change database configuration (keep it as UTC+0)
  • Don't modify Frontend (let it work normally)
  • Check existing data - may need to convert existing records

🎯 Results After Fix

Correct Timeline

  1. Frontend: 2025-06-16 13:52 (UTC+7)
  2. Sent to Backend: 2025-06-16T06:52:00.000Z (UTC) βœ…
  3. Database stores: 2025-06-16 06:52 (UTC) βœ…
  4. Backend reads: 2025-06-16T06:52:00.000Z (UTC) βœ…
  5. Swagger displays: 2025-06-16T06:52:00.000Z or 13:52 local time βœ…

Frontend Display

// React will display time in user's timezone automatically
const date = new Date("2025-06-16T06:52:00.000Z");
console.log(date.toLocaleString('en-US')); // "6/16/2025, 1:52:00 PM" (UTC+7)
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Best Practices

1. Recommended Approach

  • Always store data as UTC in database
  • Configure Backend to work in UTC
  • Let Frontend handle timezone display for users

2. Avoid

  • Don't store local timezone data in database
  • Don't let Backend convert timezones manually
  • Don't use strings for time management - use Date objects

πŸ”— References


🀝 Contributing

Found this helpful? Please star ⭐ and share with others who might face similar timezone issues!

If you have additional solutions or improvements, feel free to submit a PR or open an issue.


πŸ“„ License

This documentation is provided under MIT License. Feel free to use and modify as needed.

Top comments (1)

Collapse
 
rrakso profile image
rrakso

THANK YOU! THANK YOU!!!

To anyone else struggling. In my case, setting timezone: 'Z' didn't help because I was using a sqlserver as a driver.

For the sqlserver driver, you have to add the following to your TypeORM DataSource configuration:

options: {
    useUTC: true,
},
Enter fullscreen mode Exit fullscreen mode