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 (0)