Skip to content

Request Extensions ​

Http provides request, get, post, put, and delete. The built-in Resource is simply an example that extends Http with common request methods. Because the top-level createService() uses it by default, ordinary business APIs automatically support upload and download.

text
Http
  ↓ extend with methods
Resource
  ↓ createService
Service
  ↓ createApi
Business API.$http

How Resource Extends Http ​

The core form of Resource is:

ts
class Resource extends Http {
  upload(
    requestPath: string,
    data: FormData | UniFormData,
    config?: RequestConfig,
  ) {
    return this.request(requestPath, {
      headers: { 'content-type': 'multipart/form-data' },
      data,
      method: 'POST',
      ...config,
    })
  }

  downloadFile(requestPath: string, config?: RequestConfig) {
    return this.request<any>(requestPath, {
      responseType: 'blob',
      method: 'GET',
      ...config,
    }).then(({ data, headers }) => {
      const disposition =
        headers?.['content-disposition'] ||
        headers?.['Content-Disposition'] ||
        ''
      const pattern = /filename\*?=(?:UTF-8'')?(?:"([^"]+)"|([^;]+))/i
      const match = disposition.match(pattern)

      let filename = (match?.[1] || match?.[2])?.trim()
      if (filename) {
        try {
          filename = decodeURIComponent(filename)
        } catch {
          // Keep the original filename from the server if decoding fails.
        }
      }
      return { filename, data }
    })
  }
}

The point is not to introduce another required model, but to show that common request capabilities can be implemented directly by extending Http.

The top-level entry point is equivalent to:

ts
const service = Resource.createService(options)

Therefore, service.http and each business API's $http are independent Resource instances.

Uploading Files ​

Use FormData in browsers:

ts
const formData = new FormData()
formData.append('file', file)

await fileApi.$http.upload('avatar', formData)

When fetchAdapter receives native FormData, it removes the manually set content-type so the browser can generate a header containing the boundary.

UniApp/Taro-style platforms use a file path and field name:

ts
await fileApi.$http.upload('avatar', {
  filePath: tempFilePath,
  fileKey: 'file',
  userId: 1001,
})

Downloading Files ​

ts
const { filename, data } = await fileApi.$http.downloadFile('export')

In Web environments, data is usually a Blob and filename is parsed from Content-Disposition. With a platform Adapter, data is usually a local temporary file path and the filename must be handled according to the platform's capabilities.

Adding Request Methods ​

Extend Resource again to add PATCH, GraphQL, or other common request capabilities for the whole project:

ts
class ProjectResource extends Resource {
  patch<T>(
    requestPath: string,
    data?: Obj,
    config: RequestConfig = {},
  ) {
    return this.request<T>(requestPath, {
      ...config,
      data,
      method: 'PATCH',
    })
  }

  graphql<T>(query: string, variables?: Obj) {
    return this.post<T>('graphql', { query, variables })
  }
}

export const service = ProjectResource.createService({
  adapter: fetchAdapter,
  baseUrl: '/api',
})

Every business module created through this Service receives the new capabilities:

ts
const userApi = service.createApi('user', {
  update(data: UserInput) {
    return this.$http.patch<User>('update', data)
  },
})

Changing Existing Request Methods ​

You can also override existing methods to add project-wide behavior before or after calling super:

ts
class ProjectResource extends Resource {
  override post<T>(
    requestPath: string,
    data?: Obj,
    config: RequestConfig = {},
  ) {
    return super.post<T>(requestPath, data, {
      ...config,
      headers: {
        ...config.headers,
        'x-client': 'project-web',
      },
    })
  }
}

Only protocol- or platform-level behavior needed by multiple business modules belongs in Resource. Operations unique to one business module should remain business methods in createApi().

Using Http Directly ​

When you do not need a Service or business modules, create a request instance directly:

ts
import axios from 'axios'
import { Http } from 'api-datamodel'

const http = new Http({
  adapter: axios,
  baseUrl: '/api',
})

const health = await http.get('health')
text
new Http(options)       → obtain one request instance directly
createService(options)  → obtain a Service that can create module instances
service.createApi()     → obtain a business module with an independent request instance

Ordinary business code should still prefer Services and business APIs. Creating Http directly is useful for low-level scenarios that do not require business-module modeling. See Request Adapters for integrating different network implementations.