Mastering The Heap: How to Capture and Store Images from Fetch Responses
Published: 2024-05-02 15:02:49

As software developers we at times do not invest the time to understand how things work.

We always hear the adage "Don't reinvent the wheel."
<img src="https://media.tenor.com/znffuOjMlEsAAAAi/spongebob-squarepants-meme.gif">

This mindset can stifle creativity and a failure in advancement of our technical knowledge. This will result in less rewarding and fulfilling personal and professional lives.

To build up my previous post where our API presented an image to the browser, we will attempt to look into fetching and saving an image from an API. Why do we need this? We don't, but why not 🤷🏿.

There are many posts related to this topic, and not many approach the topic in a runtime agnostic fashion. Many examples use the DOM (Document Object Model) to accomplish this task. Today we will use a set of JavaScript runtime agnostic utilities; let's get started.

**When in doubt READTHEDOCS**

[TLDR;](#example-code)

## Security Considerations:

While we will attempt to use best practices, any code should be scrutinized. Do not use this in production without a approval from your organization, pentesting (penetration testing) to verify it follows your organizations safety guidelines.

## Requisite Knowledge:

1. Data encoding (Hexadecimal, base64, ascii, utf-8, utf-16).
2. Fetch API (outside libraries will require additional steps).
3. System I/O (input/output) calls.
4. Asynchronous vs Synchronous programming.
5. Types of media (image, audio, and video)


## The Setup

- Install depencies.
  1. curl -fsSL https://bun.sh/install | bash  
- Fetch/AJAX.
  1. Native Fetch 
  2. [node-fetch](https://www.npmjs.com/package/node-fetch).
  3. [axios](https://www.npmjs.com/package/axios)
- Setup a bun-flavored project.  

        mkdir sample
        cd sample
        bun init

- Setup [Testing](#testing)  
    - create a feature folder to house our testing. This should help prevent your shell's autocomplete suggestions when running the "test" command.  

    ```
mkdir features
    ```

    - sample test module.
![Example Test File](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sp2lzgfyrafx4l4d0j3c.png)

## Technology  

- ECMA Script
  1. [Request Interface](https://developer.mozilla.org/en-US/docs/Web/API/Request)
  2. [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
  3.  [URL Interface](https://developer.mozilla.org/en-US/docs/Web/API/URL)
  4. [Headers Interface](https://developer.mozilla.org/en-US/docs/Web/API/Headers)

- Data Producer
  1. Random image generator API. [picsum](https://picsum.photos/536/354)

## The Code

### Helper Function

1. Change from header keys from `kebab-case` to `camelCase`.
    - Package Dependents [Change Case](https://www.npmjs.com/package/change-case)
    - DIY
        -  Create a replacer function that will handle our results from the `String.replace` function. ![Change Case](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pr672a3huo25i9l8m898.png)
        - create the case change function ![Header to camelCase](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/02q2m765eza5uq347vwq.png)
        - create a function to convert case for the header's keys. 
![Process Headers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/38mk7817utabr9p0s4g3.png)

2. Create a jpeg image processing function. The call to the API will return a JPEG/JPG image, so we will not worry with other images. 
![Process Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/120zsapmy1gh6d7s3um5.png) 

3. Create a helper function to extract the content disposition header. 
![Get Filename](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3u7kseqd22elik6zy2wp.png)

### Fetching The Data  

- Prepare the object to request a new random image. 
![Fetch Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n3crwysm066q72sbwxx1.png)
- Make network request. We will be reading the raw data from the response. I do not recommend using any other library as you will have to contend with encoding as well as creating a `Buffer`, `Blob`, or `TypedArray`. So lets just cut the extra steps out. 
![Fetch Data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c6wzha6zb8m8fne7kxl9.png)

### Save The Data
- the penultimate step is to save the data. You will have to figure this part out on your own (It's just import statements, and calling the function). I am not gonna give you everything silly. 
![Save Image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/utd0mqzpta26rdah3ibg.png)
- The last step is to open the image and see what you got.

## Testing  

### **Maybe Later**  

## Example Code  

```ts
import { createWriteStream } from 'fs'
class FileNameError extends Error {
    constructor(filename:unknown) {
      super(
        `Incorrect data give for file name. Expected a string received: ${!filename ? typeof filename : filename.constructor.name}`,
      );
    }
  }
type ParsedResponse = {
    [x: string]: any;
    body?: Response;
}
/**
 * see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
 */
const mediaExtensionyTypes = new Map([
    ['jpg', 'image/jpeg']
])

const headerCaseChange = (sentence: string) => sentence.replace(/\-\w/g, (word) => word.toUpperCase().slice(1))
const getFileName = ({ contentDisposition }: Record<string, string>) => contentDisposition.split(";").reduce((accum: Record<string, string|null>, current: string)=> {
    const [key, val] = current.split("=")
    const value = JSON.parse(val?.trim()||"null")
    return {...accum, [key.trim()]: value }
}, {})
const prepareFetch = async () => new Request(new URL('https://picsum.photos/536/354'),{ mode: 'cors' })
const getHeaders = (headers: Headers) => [...headers.entries()].reduce((accum, [key, value]: string[]) => ({...accum, [headerCaseChange(key)]: value}), {})

const processJpegImage = async ({ value }: ReadableStreamReadResult<Uint8Array>) => {
    if (!value?.byteLength) return 'none'
    const [ a, b, c] = value
    if (a === 0xff && b === 0xd8 && c === 0xff) return 'jpg' 
}
const fetchImage = async (): Promise<ParsedResponse> => {
    const request = await prepareFetch()
    const {headers, body} = await fetch(request)
    const reader = body?.getReader()
    const data = await reader?.read()
    if (typeof data === 'undefined') return {}
    const extension = await processJpegImage(data)
    return { extension, headers: getHeaders(headers), data}
}
const saveResponse = async () => {
    const { headers, data, extension } = await fetchImage()
    const mediaType = mediaExtensionyTypes.get(extension) ?? 'application/octet-stream'
    const { filename } = getFileName(headers)
    if (!filename) return new FileNameError(filename)
    const stream = createWriteStream(filename)
    stream.write(data.value)
    return { extension, mediaType, filename, }
}
```  

Inspiration was for this post came from this blog post. [Saving and Image...](https://chrisfrew.in/blog/saving-images-in-node-js-using-fetch-with-array-buffer-and-buffer/)  

Resources:  

[OWASP File Upload Cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html#file-upload-validation)