Angular HttpClient และ Interceptor ในปี 2026: การจัดการ Request และคำถามสัมภาษณ์

เรียนรู้วิธีใช้ Angular HttpClient และ functional interceptor สำหรับการจัดการ HTTP request, การยืนยันตัวตนด้วย token, retry logic และ caching พร้อมคำถามสัมภาษณ์ Angular ที่พบบ่อย

Angular HttpClient และ Interceptor สำหรับการจัดการ HTTP request

Angular HttpClient interceptor เปลี่ยนแปลงวิธีที่แอปพลิเคชันจัดการ HTTP request ตั้งแต่การเพิ่ม header สำหรับการยืนยันตัวตนไปจนถึงการใช้งาน retry logic Angular 20 ได้กำหนดมาตรฐาน functional interceptor เป็นแนวทางที่แนะนำ โดยแทนที่รูปแบบ class-based ด้วย API ที่คาดเดาได้มากขึ้นและสามารถ tree-shake ได้ดีกว่า

Functional Interceptor เป็นมาตรฐาน

Angular 20 แนะนำให้ใช้ functional interceptor ด้วย withInterceptors() แทน class-based interceptor เนื่องจาก functional interceptor มีลำดับการทำงานที่คาดเดาได้มากกว่าและผสานกับ standalone component ได้ดีกว่า

การตั้งค่า HttpClient ด้วย provideHttpClient

ก่อนเขียน interceptor จำเป็นต้องกำหนดค่า HttpClient ในการ bootstrap แอปพลิเคชัน สถาปัตยกรรม standalone ของ Angular ใช้ provideHttpClient() แทน HttpClientModule แบบเดิม

การตั้งค่าทำในการกำหนดค่าแอปพลิเคชัน:

main.tstypescript
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { authInterceptor } from './interceptors/auth.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      // Interceptor ทำงานตามลำดับใน array
      withInterceptors([authInterceptor, loggingInterceptor])
    ),
  ],
});

Interceptor ทำงานตามลำดับที่กำหนดใน array โดย interceptor ตัวแรกจะประมวลผล outgoing request ก่อน และ interceptor ตัวสุดท้ายจะเห็น response ก่อน

โครงสร้างของ Functional Interceptor

Functional interceptor รับ request และฟังก์ชัน next การเรียก next(req) จะส่ง request ไปยัง interceptor ถัดไปใน chain หรือไปยัง backend หากเป็น interceptor ตัวสุดท้าย

โครงสร้างพื้นฐานมีลักษณะดังนี้:

interceptors/logging.interceptor.tstypescript
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn, HttpEvent } from '@angular/common/http';
import { Observable, tap } from 'rxjs';

export const loggingInterceptor: HttpInterceptorFn = (
  req: HttpRequest<unknown>,
  next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
  const startTime = performance.now();
  console.log(`[HTTP] ${req.method} ${req.url}`);

  return next(req).pipe(
    tap({
      next: () => {
        const duration = Math.round(performance.now() - startTime);
        console.log(`[HTTP] ${req.method} ${req.url} completed in ${duration}ms`);
      },
      error: (err) => {
        console.error(`[HTTP] ${req.method} ${req.url} failed:`, err.message);
      },
    })
  );
};

Interceptor นี้บันทึก method และ URL ของ request จากนั้นวัดเวลา response โดย operator tap จะสังเกต response โดยไม่แก้ไขมัน

Authentication Interceptor พร้อม Token Injection

แอปพลิเคชันส่วนใหญ่ต้องแนบ authentication token กับ outgoing request โดย interceptor จะ clone request พร้อม header Authorization เนื่องจากอ็อบเจกต์ HttpRequest เป็น immutable

รูปแบบนี้ดึง token จาก service และแนบมัน:

interceptors/auth.interceptor.tstypescript
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  // Inject service โดยใช้ฟังก์ชัน inject() ของ Angular
  const authService = inject(AuthService);
  const token = authService.getAccessToken();

  // ข้าม auth header สำหรับ public endpoint
  if (req.url.includes('/public/') || !token) {
    return next(req);
  }

  // Clone request พร้อม header Authorization
  const authReq = req.clone({
    setHeaders: {
      Authorization: `Bearer ${token}`,
    },
  });

  return next(authReq);
};

ฟังก์ชัน inject() ทำงานได้เพราะ interceptor ทำงานภายใน injection context ของ Angular ซึ่งช่วยลด boilerplate ของ constructor injection จาก class-based interceptor

การจัดการ Error และ Retry Logic

ความล้มเหลวของเครือข่ายสามารถเกิดขึ้นได้ตลอดเวลา Retry interceptor สามารถลองใหม่โดยอัตโนมัติสำหรับ request ที่ล้มเหลวด้วย exponential backoff ซึ่งช่วยเพิ่มความน่าเชื่อถือโดยไม่ต้องเปลี่ยนแปลงทุก HTTP call

Operator ของ RxJS จัดการ retry logic:

interceptors/retry.interceptor.tstypescript
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { retry, timer } from 'rxjs';

export const retryInterceptor: HttpInterceptorFn = (req, next) => {
  // Retry เฉพาะ GET request (idempotent)
  if (req.method !== 'GET') {
    return next(req);
  }

  return next(req).pipe(
    retry({
      count: 3,
      delay: (error: HttpErrorResponse, retryCount: number) => {
        // ไม่ retry client error (4xx)
        if (error.status >= 400 && error.status < 500) {
          throw error;
        }
        // Exponential backoff: 1s, 2s, 4s
        const delayMs = Math.pow(2, retryCount - 1) * 1000;
        console.log(`Retry #${retryCount} in ${delayMs}ms`);
        return timer(delayMs);
      },
    })
  );
};

Interceptor นี้ retry เฉพาะ GET request เท่านั้น เนื่องจาก POST, PUT และ DELETE ไม่ใช่ idempotent ส่วน client error (4xx) จะข้าม retry เพราะ server ปฏิเสธ request อย่างชัดเจนแล้ว

พร้อมที่จะพิชิตการสัมภาษณ์ Angular แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

Response Caching ด้วย HttpContext

HttpClient มี HttpContext สำหรับส่ง metadata ระหว่าง interceptor และโค้ดที่เรียกใช้ Caching interceptor สามารถอ่าน context token เพื่อกำหนดพฤติกรรม cache

Cache token และ interceptor ทำงานร่วมกัน:

interceptors/cache.interceptor.tstypescript
import { HttpInterceptorFn, HttpContextToken, HttpResponse } from '@angular/common/http';
import { of, tap } from 'rxjs';

// Context token สำหรับควบคุม caching ต่อ request
export const CACHE_REQUEST = new HttpContextToken<boolean>(() => false);

const cache = new Map<string, HttpResponse<unknown>>();

export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
  // Cache เฉพาะเมื่อร้องขออย่างชัดเจนผ่าน context
  if (!req.context.get(CACHE_REQUEST) || req.method !== 'GET') {
    return next(req);
  }

  const cacheKey = req.urlWithParams;
  const cachedResponse = cache.get(cacheKey);

  if (cachedResponse) {
    console.log(`[Cache] HIT: ${cacheKey}`);
    return of(cachedResponse.clone());
  }

  return next(req).pipe(
    tap((event) => {
      if (event instanceof HttpResponse) {
        console.log(`[Cache] STORE: ${cacheKey}`);
        cache.set(cacheKey, event.clone());
      }
    })
  );
};

โค้ดที่เรียกใช้เลือกเปิด caching ด้วย context token:

services/user.service.tstypescript
import { HttpClient, HttpContext } from '@angular/common/http';
import { CACHE_REQUEST } from '../interceptors/cache.interceptor';

export class UserService {
  constructor(private http: HttpClient) {}

  getUser(id: string) {
    return this.http.get(`/api/users/${id}`, {
      context: new HttpContext().set(CACHE_REQUEST, true),
    });
  }
}

รูปแบบนี้ทำให้พฤติกรรม caching ชัดเจนที่ call site แทนที่จะซ่อนอยู่ในการกำหนดค่า global

Token Refresh พร้อมการจัดการ 401

เมื่อ access token หมดอายุ server จะส่ง response 401 กลับมา Interceptor สามารถจับสิ่งนี้ refresh token และลอง request เดิมใหม่อย่างโปร่งใส

Logic การ refresh ประสานงานหลาย concurrent request:

interceptors/token-refresh.interceptor.tstypescript
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError, BehaviorSubject, filter, take } from 'rxjs';
import { AuthService } from '../services/auth.service';

let isRefreshing = false;
const refreshTokenSubject = new BehaviorSubject<string | null>(null);

export const tokenRefreshInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);

  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status !== 401 || req.url.includes('/auth/refresh')) {
        return throwError(() => error);
      }

      if (isRefreshing) {
        // รอให้ refresh เสร็จ
        return refreshTokenSubject.pipe(
          filter((token) => token !== null),
          take(1),
          switchMap((token) => {
            const retryReq = req.clone({
              setHeaders: { Authorization: `Bearer ${token}` },
            });
            return next(retryReq);
          })
        );
      }

      isRefreshing = true;
      refreshTokenSubject.next(null);

      return authService.refreshToken().pipe(
        switchMap((response) => {
          isRefreshing = false;
          refreshTokenSubject.next(response.accessToken);
          const retryReq = req.clone({
            setHeaders: { Authorization: `Bearer ${response.accessToken}` },
          });
          return next(retryReq);
        }),
        catchError((refreshError) => {
          isRefreshing = false;
          authService.logout();
          return throwError(() => refreshError);
        })
      );
    })
  );
};

BehaviorSubject ป้องกันการเรียก refresh หลายครั้งเมื่อหลาย request ล้มเหลวพร้อมกัน หลังจาก token ถูก refresh แล้ว request ที่รอทั้งหมดจะลองใหม่ด้วย token ใหม่

คำถามสัมภาษณ์: HttpClient และ Interceptor

การสัมภาษณ์ทางเทคนิคมักทดสอบความเข้าใจเกี่ยวกับรูปแบบการจัดการ HTTP คำถามเหล่านี้ปรากฏบ่อยในตำแหน่ง Angular

Q: ทำไม functional interceptor จึงถูกแนะนำมากกว่า class-based interceptor?

Functional interceptor มีลำดับการทำงานที่คาดเดาได้มากกว่า เอกสาร Angular ระบุว่า functional interceptor ด้วย withInterceptors() รักษาลำดับ array อย่างเคร่งครัด ในขณะที่ DI-based interceptor อาจมีปัญหาเรื่องลำดับในลำดับชั้น module ที่ซับซ้อน Functional interceptor ยังสร้าง bundle ที่เล็กกว่าเพราะ tree-shake ได้ดีกว่า

Q: จะป้องกันไม่ให้ interceptor ทำงานกับ request บางตัวได้อย่างไร?

มีสองวิธี วิธีแรก ตรวจสอบรูปแบบ URL ใน interceptor และเรียก next(req) โดยไม่แก้ไขสำหรับ path ที่ต้องการยกเว้น วิธีที่สอง ใช้ token HttpContext ที่โค้ดเรียกใช้ตั้งค่าเพื่อ opt-out จากพฤติกรรม interceptor วิธี context จะเก็บการตัดสินใจยกเว้นไว้ที่ call site

Q: จะเกิดอะไรขึ้นถ้าลืมเรียก next() ใน interceptor?

Request จะไม่ถึง server Observable ที่ส่งกลับโดย next() แสดงถึงส่วนที่เหลือของ interceptor chain และ HTTP call จริง ถ้าไม่เรียกมัน interceptor downstream และ backend จะไม่เห็น request นี้เป็นการออกแบบโดยตั้งใจสำหรับ caching interceptor ที่ส่งคืน cached response

Q: จะจัดการ concurrent request ระหว่างการ token refresh ได้อย่างไร?

ใช้ BehaviorSubject เพื่อประสานงาน เมื่อ 401 แรกเรียก refresh ให้ตั้ง flag และ emit null บน subject 401 ถัดไปจะรอบน subject ด้วย filter จนกว่า token ใหม่จะมาถึง วิธีนี้ป้องกันการเรียก refresh หลายครั้งและรับประกันว่า pending request ทั้งหมดจะลองใหม่ด้วย token ใหม่เดียวกัน

Q: Interceptor สามารถแก้ไข response body ได้หรือไม่?

ได้ ใช้ map() บน Observable ที่ส่งกลับโดย next() เพื่อแปลงอ็อบเจกต์ HttpResponse เนื่องจาก response เป็น immutable จึงต้อง clone พร้อม body ที่แก้ไขแล้ว รูปแบบนี้ใช้ได้สำหรับการเพิ่ม computed field, unwrap API envelope หรือ normalize โครงสร้างข้อมูล

สำหรับการเตรียมสัมภาษณ์ Angular เพิ่มเติม ดู โมดูลคำถามสัมภาษณ์ HttpClient

การทดสอบ Functional Interceptor

Interceptor ต้องการการทดสอบด้วย mocked HTTP handler HttpTestingController ของ Angular ทำงานกับ interceptor ที่ลงทะเบียนผ่าน provideHttpClient()

การตั้งค่าการทดสอบมีลักษณะดังนี้:

interceptors/auth.interceptor.spec.tstypescript
import { TestBed } from '@angular/core/testing';
import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { authInterceptor } from './auth.interceptor';
import { AuthService } from '../services/auth.service';

describe('authInterceptor', () => {
  let httpClient: HttpClient;
  let httpTesting: HttpTestingController;
  let authServiceSpy: jasmine.SpyObj<AuthService>;

  beforeEach(() => {
    authServiceSpy = jasmine.createSpyObj('AuthService', ['getAccessToken']);

    TestBed.configureTestingModule({
      providers: [
        provideHttpClient(withInterceptors([authInterceptor])),
        provideHttpClientTesting(),
        { provide: AuthService, useValue: authServiceSpy },
      ],
    });

    httpClient = TestBed.inject(HttpClient);
    httpTesting = TestBed.inject(HttpTestingController);
  });

  it('should add Authorization header when token exists', () => {
    authServiceSpy.getAccessToken.and.returnValue('test-token');

    httpClient.get('/api/data').subscribe();

    const req = httpTesting.expectOne('/api/data');
    expect(req.request.headers.get('Authorization')).toBe('Bearer test-token');
    req.flush({ data: 'test' });
  });

  it('should skip Authorization for public endpoints', () => {
    authServiceSpy.getAccessToken.and.returnValue('test-token');

    httpClient.get('/public/health').subscribe();

    const req = httpTesting.expectOne('/public/health');
    expect(req.request.headers.has('Authorization')).toBeFalse();
    req.flush({ status: 'ok' });
  });
});

การทดสอบตรวจสอบทั้ง header injection และข้อยกเว้น public endpoint โดยไม่ทำ network request จริง

รูปแบบ Production: การรวม Interceptor

แอปพลิเคชันจริงซ้อนหลาย interceptor ลำดับสำคัญ: authentication ควรทำงานก่อน logging และ caching ควรทำงานก่อน retry logic

การกำหนดค่า production อาจมีลักษณะดังนี้:

main.tstypescript
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { authInterceptor } from './interceptors/auth.interceptor';
import { tokenRefreshInterceptor } from './interceptors/token-refresh.interceptor';
import { cacheInterceptor } from './interceptors/cache.interceptor';
import { retryInterceptor } from './interceptors/retry.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([
        // ลำดับ: auth -> refresh -> cache -> retry -> logging
        authInterceptor,
        tokenRefreshInterceptor,
        cacheInterceptor,
        retryInterceptor,
        loggingInterceptor,
      ])
    ),
  ],
});

ลำดับนี้รับประกันว่า token จะถูกแนบก่อนการจัดการ refresh, การตรวจสอบ cache เกิดขึ้นก่อน retry และ logging จับ timing สุดท้าย

สำหรับรูปแบบ Angular ที่กว้างขึ้น ดู โมดูลพื้นฐาน RxJS ซึ่งครอบคลุม operator ที่ใช้ใน interceptor

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

ประเด็นสำคัญสำหรับ Angular HttpClient Interceptor

  • Functional interceptor ด้วย withInterceptors() เป็นมาตรฐาน Angular 20 มีลำดับที่คาดเดาได้และ tree-shaking ที่ดีกว่าทางเลือก class-based
  • ใช้ inject() ภายใน interceptor เพื่อเข้าถึง service โดยไม่ต้องมี boilerplate constructor
  • Clone request ด้วย req.clone() เพื่อแก้ไข header เนื่องจากอ็อบเจกต์ HttpRequest เป็น immutable
  • Token HttpContext ทำให้พฤติกรรมต่อ request ชัดเจนที่ call site แทนที่จะซ่อนในการกำหนดค่า global
  • Interceptor สำหรับ token refresh ควรประสานงาน concurrent request ด้วย BehaviorSubject เพื่อป้องกันการเรียก refresh หลายครั้ง
  • ลำดับ interceptor สำคัญ: authentication ก่อน caching, caching ก่อน retry, logging สุดท้าย
ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Angular เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 22 สิงหาคม 2569

แท็ก

#angular
#httpclient
#interceptor
#http
#typescript

แชร์

บทความที่เกี่ยวข้อง

Angular Dependency Injection Advanced Providers Tokens

Dependency Injection ขั้นสูงใน Angular 2026: Providers, Tokens และคำถามสัมภาษณ์

เรียนรู้ระบบ dependency injection ของ Angular อย่างลึกซึ้ง รวมถึงกลยุทธ์ provider, InjectionToken, injector แบบลำดับชั้น และคำถามสัมภาษณ์ที่พบบ่อยสำหรับนักพัฒนา Angular ที่มีประสบการณ์

RxJS ใน Angular 2026 โอเปอเรเตอร์ Subject และ interop Signals

RxJS ใน Angular 2026: โอเปอเรเตอร์ Subject และ interop Signals

RxJS ใน Angular 2026: เชี่ยวชาญโอเปอเรเตอร์ Subject และรูปแบบ interop Signals ที่นักพัฒนา Angular ใช้จริงในโปรดักชัน พร้อมคำถามสัมภาษณ์ที่พบบ่อยที่สุด

บทเรียน Angular 20 Resource API และ httpResource สำหรับการดึงข้อมูลเชิงปฏิกิริยาด้วย signal

Angular 20 ในปี 2026: Resource API, httpResource และคำถามสัมภาษณ์

Angular 20 เปิดตัว httpResource และทำให้ Resource API เสถียรสำหรับการดึงข้อมูลแบบอิงกับ signal บทเรียนเชิงปฏิบัติครอบคลุม resource(), rxResource(), httpResource(), การตรวจสอบด้วย Zod และคำถามสัมภาษณ์ที่พบบ่อย