Skip to content

Commit

Permalink
✨ Added an robust and functional http service using axios
Browse files Browse the repository at this point in the history
  • Loading branch information
ICEatm committed Jun 14, 2024
1 parent edbc00c commit 3c1936e
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 5 deletions.
52 changes: 47 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/axios": "^3.0.2",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"axios": "^1.7.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"helmet": "^7.1.0",
Expand Down
165 changes: 165 additions & 0 deletions src/common/services/http.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { Test, TestingModule } from '@nestjs/testing';
import { HttpService } from './http.service';
import { HttpService as NestHttpService, HttpModule } from '@nestjs/axios';
import { AxiosResponse, InternalAxiosRequestConfig } from 'axios';
import { of, throwError } from 'rxjs';
import { HttpException } from '@nestjs/common';

describe('HttpService', () => {
let service: HttpService;
let httpService: NestHttpService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [HttpModule],
providers: [HttpService],
}).compile();

service = module.get<HttpService>(HttpService);
httpService = module.get<NestHttpService>(NestHttpService);
});

describe('get', () => {
it('should return data on successful GET request', async () => {
const result: AxiosResponse = {
data: { message: 'success' },
status: 200,
statusText: 'OK',
headers: {},
config: {} as InternalAxiosRequestConfig,
};
jest.spyOn(httpService, 'get').mockImplementation(() => of(result));

expect(await service.get('https://example.com')).toEqual(result);
});

it('should throw an HttpException on GET request failure', async () => {
jest.spyOn(httpService, 'get').mockImplementation(() =>
throwError({
response: {
status: 404,
data: 'Not Found',
},
}),
);

await expect(service.get('https://example.com')).rejects.toThrow(
new HttpException(
{
status: 404,
message: 'Not Found',
},
404,
),
);
});
});

describe('post', () => {
it('should return data on successful POST request', async () => {
const result: AxiosResponse = {
data: { message: 'success' },
status: 201,
statusText: 'Created',
headers: {},
config: {} as InternalAxiosRequestConfig,
};
jest.spyOn(httpService, 'post').mockImplementation(() => of(result));

expect(await service.post('https://example.com', {})).toEqual(result);
});

it('should throw an HttpException on POST request failure', async () => {
jest.spyOn(httpService, 'post').mockImplementation(() =>
throwError({
response: {
status: 400,
data: 'Bad Request',
},
}),
);

await expect(service.post('https://example.com', {})).rejects.toThrow(
new HttpException(
{
status: 400,
message: 'Bad Request',
},
400,
),
);
});
});

describe('put', () => {
it('should return data on successful PUT request', async () => {
const result: AxiosResponse = {
data: { message: 'success' },
status: 200,
statusText: 'OK',
headers: {},
config: {} as InternalAxiosRequestConfig,
};
jest.spyOn(httpService, 'put').mockImplementation(() => of(result));

expect(await service.put('https://example.com', {})).toEqual(result);
});

it('should throw an HttpException on PUT request failure', async () => {
jest.spyOn(httpService, 'put').mockImplementation(() =>
throwError({
response: {
status: 401,
data: 'Unauthorized',
},
}),
);

await expect(service.put('https://example.com', {})).rejects.toThrow(
new HttpException(
{
status: 401,
message: 'Unauthorized',
},
401,
),
);
});
});

describe('delete', () => {
it('should return data on successful DELETE request', async () => {
const result: AxiosResponse = {
data: { message: 'success' },
status: 200,
statusText: 'OK',
headers: {},
config: {} as InternalAxiosRequestConfig,
};
jest.spyOn(httpService, 'delete').mockImplementation(() => of(result));

expect(await service.delete('https://example.com')).toEqual(result);
});

it('should throw an HttpException on DELETE request failure', async () => {
jest.spyOn(httpService, 'delete').mockImplementation(() =>
throwError({
response: {
status: 403,
data: 'Forbidden',
},
}),
);

await expect(service.delete('https://example.com')).rejects.toThrow(
new HttpException(
{
status: 403,
message: 'Forbidden',
},
403,
),
);
});
});
});
70 changes: 70 additions & 0 deletions src/common/services/http.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { HttpService as NestHttpService } from '@nestjs/axios';
import { AxiosRequestConfig, AxiosResponse } from 'axios';
import { catchError, lastValueFrom } from 'rxjs';

@Injectable()
export class HttpService {
constructor(private readonly httpService: NestHttpService) {}

async get(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse> {
return this.handleRequest(this.httpService.get(url, config));
}

async post(
url: string,
data: any,
config?: AxiosRequestConfig,
): Promise<AxiosResponse> {
return this.handleRequest(this.httpService.post(url, data, config));
}

async put(
url: string,
data: any,
config?: AxiosRequestConfig,
): Promise<AxiosResponse> {
return this.handleRequest(this.httpService.put(url, data, config));
}

async delete(
url: string,
config?: AxiosRequestConfig,
): Promise<AxiosResponse> {
return this.handleRequest(this.httpService.delete(url, config));
}

private async handleRequest(request: any): Promise<AxiosResponse> {
return await lastValueFrom(
request.pipe(
catchError((error) => {
if (error.response) {
throw new HttpException(
{
status: error.response.status,
message: error.response.data,
},
error.response.status,
);
} else if (error.request) {
throw new HttpException(
{
status: HttpStatus.SERVICE_UNAVAILABLE,
message: 'No response received from server',
},
HttpStatus.SERVICE_UNAVAILABLE,
);
} else {
throw new HttpException(
{
status: HttpStatus.INTERNAL_SERVER_ERROR,
message: error.message,
},
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
}),
),
);
}
}

0 comments on commit 3c1936e

Please sign in to comment.