Challenge #11: Production Code
You’re finally ready to implement the production HttpClient.requestAsync()
method. The code already exists; you just need to move it out of the test and into production. That’s what you’ll do in this challenge.
Instructions
- Move the HTTP request logic out of the test and into
HttpClient.requestAsync()
. - Create a
requestAsync()
test helper with default values for all thehttpClient.requestAsync()
parameters. Use the following signature:async function requestAsync( host = HOST, port = PORT, method = "GET", path = "/irrelevant/path", headers = undefined, body = undefined, ) { // ... return { response }; }
- Refactor the production code to tidy it up.
Remember to commit your changes when you’re done.
API Documentation
const httpClient = HttpClient.create();
Create an HttpClient
.
- returns httpClient
(HttpClient)
- the HTTP client
JavaScript Primers
Hints
1
Before you can factor out the method, you have to genericize the code you’re going to extract.
Introduce variables for all the test-specific values. Your editor might have an automatic refactoring to help you do this.
You need to introduce variables for the http.request()
parameters and the request body.
it("performs request", async () => {
spyServer.setResponse({
status: 999,
headers: {
myResponseHeader: "myResponseValue",
},
body: "my response body"
});
const host = HOST;
const port = PORT;
const method = "POST";
const path = "/my/path";
const headers = {
myRequestHeader: "myRequestValue",
};
const body = "my request body";
const clientRequest = http.request({
host: host,
port: port,
method: method,
path: path,
headers: headers,
});
clientRequest.end(body);
const response = await new Promise((resolve, reject) => { // add <HttpClientResponse> in TypeScript
clientRequest.on("response", (clientResponse) => {
let body = "";
clientResponse.on("data", (chunk) => {
body += chunk;
});
clientResponse.on("end", () => {
resolve({
status: clientResponse.statusCode, // add 'as number' in TypeScript
headers: clientResponse.headers, // add 'as HttpHeaders' in TypeScript
body,
});
});
});
});
assert.deepEqual(spyServer.lastRequest, {
method: "POST",
path: "/my/path",
headers: {
myrequestheader: "myRequestValue",
host: `${HOST}:${PORT}`,
connection: "close",
"content-length": "15",
},
body: "my request body",
});
delete response.headers.date;
assert.deepEqual(response, {
status: 999,
headers: {
myresponseheader: "myResponseValue",
connection: "close",
"content-length": "16",
},
body: "my response body",
});
});
2
Now you can extract the request code into a function.
You need the http.request()
code and the response handler promise.
Your editor might have an “Extract Function” or “Extract Method” refactoring that will help.
async function requestAsync(host, port, method, path, headers, body) {
const clientRequest = http.request({
host: host,
port: port,
method: method,
path: path,
headers: headers,
});
clientRequest.end(body);
const response = await new Promise((resolve, reject) => { // add <HttpClientResponse> for TypeScript
clientRequest.on("response", (clientResponse) => {
let body = "";
clientResponse.on("data", (chunk) => {
body += chunk;
});
clientResponse.on("end", () => {
resolve({
status: clientResponse.statusCode, // add 'as number' for TypeScript
headers: clientResponse.headers, // add 'as HttpHeaders' for TypeScript
body,
});
});
});
});
return response;
}
it("performs request", async () => {
spyServer.setResponse({
status: 999,
headers: {
myResponseHeader: "myResponseValue",
},
body: "my response body"
});
const host = HOST;
const port = PORT;
const method = "POST";
const path = "/my/path";
const headers = {
myRequestHeader: "myRequestValue",
};
const body = "my request body";
const response = await requestAsync(host, port, method, path, headers, body);
assert.deepEqual(spyServer.lastRequest, {
method: "POST",
path: "/my/path",
headers: {
myrequestheader: "myRequestValue",
host: `${HOST}:${PORT}`,
connection: "close",
"content-length": "15",
},
body: "my request body",
});
delete response.headers.date;
assert.deepEqual(response, {
status: 999,
headers: {
myresponseheader: "myResponseValue",
connection: "close",
"content-length": "16",
},
body: "my response body",
});
});
(The requestAsync()
call uses object destructuring.)
TypeScript requires type declarations for the new function:
async function requestAsync(
host: string,
port: number,
method: string,
path: string,
headers: HttpHeaders,
body: string,
) {
const clientRequest = http.request({
host: host,
port: port,
method: method,
path: path,
headers: headers,
});
clientRequest.end(body);
const response = await new Promise<HttpClientResponse>((resolve, reject) => {
clientRequest.on("response", (clientResponse) => {
let body = "";
clientResponse.on("data", (chunk) => {
body += chunk;
});
clientResponse.on("end", () => {
resolve({
status: clientResponse.statusCode as number,
headers: clientResponse.headers as HttpHeaders,
body,
});
});
});
});
return response;
}
3
You’re ready to move the request code to production.
Cut and paste the requestAsync()
body in the tests into the body of the production httpClient.requestAsync()
.
In the test’s requestAsync()
, you’ll need to instantiate the client and call httpClient.requestAsync()
. Don’t forget to await
it.
You can instantiate it with HttpClient.create()
.
You’ll need to pass through all the parameters and return the return value.
// Test code
async function requestAsync(host, port, method, path, headers, body) {
const client = HttpClient.create();
return await client.requestAsync({ host, port, method, path, headers, body });
}
// Production code (no changes other than being moved)
async requestAsync({ host, port, method, path, headers, body }) {
const clientRequest = http.request({
host: host,
port: port,
method: method,
path: path,
headers: headers,
});
clientRequest.end(body);
const response = await new Promise((resolve, reject) => { // add <HttpClientResponse> for TypeScript
clientRequest.on("response", (clientResponse) => {
let body = "";
clientResponse.on("data", (chunk) => {
body += chunk;
});
clientResponse.on("end", () => {
resolve({
status: clientResponse.statusCode, // add 'as number' for TypeScript
headers: clientResponse.headers, // add 'as HttpHeaders' for TypeScript
body,
});
});
});
});
return response;
}
4
You still need to add default function parameters to the test’s requestAsync()
helper.
This is a good opportunity to inline the test variables, too.
async function requestAsync({
host = HOST,
port = PORT,
method = "GET",
path = "/irrelevant/path",
headers = {},
body = "irrelevant body",
} = {}) {
const client = HttpClient.create();
const response = await client.requestAsync({ host, port, method, path, headers, body });
return { response };
}
it("performs request", async () => {
spyServer.setResponse({
status: 999,
headers: {
myResponseHeader: "myResponseValue",
},
body: "my response body"
});
const { response } = await requestAsync({
host: HOST,
port: PORT,
method: "POST",
path: "/my/path",
headers: {
myRequestHeader: "myRequestValue",
},
body: "my request body",
});
assert.deepEqual(spyServer.lastRequest, {
method: "POST",
path: "/my/path",
headers: {
myrequestheader: "myRequestValue",
host: `${HOST}:${PORT}`,
connection: "close",
"content-length": "15",
},
body: "my request body",
});
delete response.headers.date;
assert.deepEqual(response, {
status: 999,
headers: {
myresponseheader: "myResponseValue",
connection: "close",
"content-length": "16",
},
body: "my response body",
});
});
The TypeScript code no longer needs type declarations, so it’s the same as the JavaScript code.
5
Now you can refactor the production code. This is a matter of taste, with no right answer.
One approach is to factor the request and response handling into separate private methods.
async requestAsync({ host, port, method, path, headers, body }) {
const clientRequest = this.#sendRequest(host, port, method, path, headers, body);
return await this.#handleResponseAsync(clientRequest);
}
#sendRequest(host, port, method, path, headers, body) {
const clientRequest = http.request({
host: host,
port: port,
method: method,
path: path,
headers: headers,
});
clientRequest.end(body);
return clientRequest;
}
async #handleResponseAsync(clientRequest) {
return await new Promise((resolve, reject) => {
clientRequest.on("response", (clientResponse) => {
let body = "";
clientResponse.on("data", (chunk) => {
body += chunk;
});
clientResponse.on("end", () => {
resolve({
status: clientResponse.statusCode,
headers: clientResponse.headers,
body,
});
});
});
});
}
TypeScript requires type declarations for the new methods:
async requestAsync(
{ host, port, method, path, headers, body }: HttpClientRequestParameters
): Promise<HttpClientResponse> {
const clientRequest = this.#sendRequest(host, port, method, path, headers, body);
return await this.#handleResponseAsync(clientRequest);
}
#sendRequest(
host: string,
port: number,
method: string,
path: string,
headers?: HttpHeaders,
body?: string
): http.ClientRequest {
const clientRequest = http.request({
host: host,
port: port,
method: method,
path: path,
headers: headers,
});
clientRequest.end(body);
return clientRequest;
}
async #handleResponseAsync(clientRequest: http.ClientRequest): Promise<HttpClientResponse> {
return await new Promise((resolve, reject) => {
clientRequest.on("response", (clientResponse) => {
let body = "";
clientResponse.on("data", (chunk) => {
body += chunk;
});
clientResponse.on("end", () => {
resolve({
status: clientResponse.statusCode as number,
headers: clientResponse.headers as HttpHeaders,
body,
});
});
});
});
}
Complete Solution
Test code (JavaScript):
describe.only("HTTP Client", () => {
// ...
it("performs request", async () => {
spyServer.setResponse({
status: 999,
headers: {
myResponseHeader: "myResponseValue",
},
body: "my response body"
});
const { response } = await requestAsync({
host: HOST,
port: PORT,
method: "POST",
path: "/my/path",
headers: {
myRequestHeader: "myRequestValue",
},
body: "my request body",
});
assert.deepEqual(spyServer.lastRequest, {
method: "POST",
path: "/my/path",
headers: {
myrequestheader: "myRequestValue",
host: `${HOST}:${PORT}`,
connection: "close",
"content-length": "15",
},
body: "my request body",
});
delete response.headers.date;
assert.deepEqual(response, {
status: 999,
headers: {
myresponseheader: "myResponseValue",
connection: "close",
"content-length": "16",
},
body: "my response body",
});
});
// ...
});
async function requestAsync({
host = HOST,
port = PORT,
method = "GET",
path = "/irrelevant/path",
headers = undefined,
body = undefined,
} = {}) {
const client = HttpClient.create();
const response = await client.requestAsync({ host, port, method, path, headers, body });
return { response };
}
Test code (TypeScript):
describe.only("HTTP Client", () => {
// ...
it("performs request", async () => {
spyServer.setResponse({
status: 999,
headers: {
myResponseHeader: "myResponseValue",
},
body: "my response body"
});
const { response } = await requestAsync({
host: HOST,
port: PORT,
method: "POST",
path: "/my/path",
headers: {
myRequestHeader: "myRequestValue",
},
body: "my request body",
});
assert.deepEqual(spyServer.lastRequest, {
method: "POST",
path: "/my/path",
headers: {
myrequestheader: "myRequestValue",
host: `${HOST}:${PORT}`,
connection: "close",
"content-length": "15",
},
body: "my request body",
});
delete response.headers.date;
assert.deepEqual(response, {
status: 999,
headers: {
myresponseheader: "myResponseValue",
connection: "close",
"content-length": "16",
},
body: "my response body",
});
});
// ...
});
async function requestAsync({
host = HOST,
port = PORT,
method = "GET",
path = "/irrelevant/path",
headers = undefined,
body = undefined,
} = {}) {
const client = HttpClient.create();
const response = await client.requestAsync({ host, port, method, path, headers, body });
return { response };
}
Production code (JavaScript):
export class HttpClient {
// ...
async requestAsync({ host, port, method, path, headers, body }) {
const clientRequest = this.#sendRequest(host, port, method, path, headers, body);
return await this.#handleResponseAsync(clientRequest);
}
#sendRequest(host, port, method, path, headers, body) {
const clientRequest = http.request({
host: host,
port: port,
method: method,
path: path,
headers: headers,
});
clientRequest.end(body);
return clientRequest;
}
async #handleResponseAsync(clientRequest) {
return await new Promise((resolve, reject) => {
clientRequest.on("response", (clientResponse) => {
let body = "";
clientResponse.on("data", (chunk) => {
body += chunk;
});
clientResponse.on("end", () => {
resolve({
status: clientResponse.statusCode,
headers: clientResponse.headers,
body,
});
});
});
});
}
}
Production code (TypeScript):
export class HttpClient {
// ...
async requestAsync(
{ host, port, method, path, headers, body }: HttpClientRequestParameters
): Promise<HttpClientResponse> {
const clientRequest = this.#sendRequest(host, port, method, path, headers, body);
return await this.#handleResponseAsync(clientRequest);
}
#sendRequest(
host: string,
port: number,
method: string,
path: string,
headers?: HttpHeaders,
body?: string
): http.ClientRequest {
const clientRequest = http.request({
host: host,
port: port,
method: method,
path: path,
headers: headers,
});
clientRequest.end(body);
return clientRequest;
}
async #handleResponseAsync(clientRequest: http.ClientRequest): Promise<HttpClientResponse> {
return await new Promise((resolve, reject) => {
clientRequest.on("response", (clientResponse) => {
let body = "";
clientResponse.on("data", (chunk) => {
body += chunk;
});
clientResponse.on("end", () => {
resolve({
status: clientResponse.statusCode as number,
headers: clientResponse.headers as HttpHeaders,
body,
});
});
});
});
}
// ...
}