Skip to main content

Using bodies

Bodies are used to send data in a request and receive data in a response. Request bodies are commonly used to include a payload of data that the server will process, whereas response bodies may contain the result returned by the server. A body can contain various types of data, such as JSON, XML, and binary data such as images and files.

Using request bodies​

Request bodies are declared in your schema using the request.body property.

JSON request body​

JSON bodies are one of the most common ways to send data in requests. To use a JSON body in a request, declare its type in your schema.

schema.ts
import { HttpSchema } from '@zimic/http';

interface User {
id: string;
username: string;
}

type Schema = HttpSchema<{
'/users': {
POST: {
request: {
body: { username: string };
};
response: {
201: { body: User };
};
};
};
}>;

Then, use the body option to send the data in your fetch request. Note that you should set the content-type header to application/json to indicate that the body is in JSON format, otherwise it may be interpreted as a plain text. Also, serialize the body with JSON.stringify() before sending it.

import { createFetch } from '@zimic/fetch';

const fetch = createFetch<Schema>({
baseURL: 'http://localhost:3000',
});

const response = await fetch('/users', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username: 'me' }),
});
TIP: content-type header inference

@zimic/http automatically infers the type of the content-type header as application/json if the request body is a JSON type. You can override this behavior by explicitly setting a different type in your schema.

schema.ts
type Schema = HttpSchema<{
'/users': {
POST: {
request: {
headers: {
'content-type': 'application/json; charset=utf-8';
};
body: { username: string };
};
response: {
201: { body: User };
};
};
};
}>;

The inference is limited to typing, so you still need to set the header when making requests with JSON bodies. This follows behavior of the Fetch API.

FormData request body​

FormData is a special type of body to construct a set of key-value pairs with variable types of data. A common use case is to upload files to a server.

To send a FormData body, declare its type in your schema. Use the HttpFormData to indicate that the body is a FormData type.

import { HttpFormData, HttpSchema } from '@zimic/http';

interface AvatarFormDataSchema {
image: File;
}

type Schema = HttpSchema<{
'/users/:userId/avatar': {
PUT: {
request: {
headers?: { 'content-type'?: 'multipart/form-data' };
body: HttpFormData<AvatarFormDataSchema>;
};
response: {
200: { body: { url: string } };
};
};
};
}>;

After that, create an HttpFormData instance and add the data using set or append.

import { HttpFormData } from '@zimic/http';
import { createFetch } from '@zimic/fetch';

const fetch = createFetch<Schema>({
baseURL: 'http://localhost:3000',
});

// Getting an uploaded file from an input element
const imageInput = document.querySelector<HTMLInputElement>('input[type="file"]');
const imageFile = imageInput!.files![0];

const formData = new HttpFormData<AvatarFormDataSchema>();
formData.append('image', imageFile);

const response = await fetch(`/users/${userId}/avatar`, {
method: 'PUT',
headers: { 'content-type': 'multipart/form-data' },
body: formData,
});

Depending on your runtime, the content-type header may be set automatically when using a FormData body. In that case, you don't need to set it manually.

const response = await fetch(`/users/${userId}/avatar`, {
method: 'PUT',
body: formData,
});

Binary request body​

Binary bodies are used to send raw binary data in requests. To send a binary body, declare its type in your schema. Blob, ArrayBuffer, and ReadableStream are common types for binary data.

schema.ts
import { HttpSchema } from '@zimic/http';

interface Video {
id: string;
url: string;
}

type Schema = HttpSchema<{
'/upload': {
POST: {
request: {
headers?: { 'content-type'?: string };
body: Blob;
};
};
response: {
201: { body: Video };
};
};
}>;

Then, use the body option to send the data in your fetch request. Make sure to set the content-type header, such as video/mp4, image/png, or application/octet-stream for generic binary data. Learn more about MIME types to use in your requests.

import fs from 'fs';
import { createFetch } from '@zimic/fetch';

const fetch = createFetch<Schema>({
baseURL: 'http://localhost:3000',
});

// Getting a file from the file system
const videoBuffer = await fs.promises.readFile('video.mp4');
const videoFile = new File([videoBuffer], 'video.mp4');

const response = await fetch('/upload', {
method: 'POST',
headers: { 'content-type': 'video/mp4' },
body: videoFile,
});

Request streaming​

When working with large files or chunked data, you can use ReadableStream and duplex: 'half' to stream the request body. This allows you to send data in smaller parts, which can be more efficient and reduce memory usage.

schema.ts
import { HttpSchema } from '@zimic/http';

interface Video {
id: string;
url: string;
}

type Schema = HttpSchema<{
'/upload': {
POST: {
request: {
headers?: { 'content-type'?: string };
body: ReadableStream;
};
};
response: {
201: { body: Video };
};
};
}>;
import fs from 'fs';
import { Readable } from 'stream';
import { createFetch } from '@zimic/fetch';

const fetch = createFetch<Schema>({
baseURL: 'http://localhost:3000',
});

// Streaming a file from the file system
const fileStream = fs.createReadStream('video.mp4');
const requestStream = Readable.toWeb(fileStream);

const response = await fetch('/upload', {
method: 'POST',
headers: { 'content-type': 'video/mp4' },
body: requestStream,
duplex: 'half',
});

Plain-text request body​

Plain-text bodies can be declared as a string.

import { HttpSchema } from '@zimic/http';

type Schema = HttpSchema<{
'/content': {
POST: {
request: {
body: string;
};
response: {
201: {};
};
};
};
}>;

After that, send a plain-text body as a string in your fetch request.

const response = await fetch('/content', {
method: 'POST',
body: 'text',
});

URL-encoded request body​

Bodies with URL-encoded data can be declared with HttpSearchParams.

import { HttpSchema, HttpSearchParams } from '@zimic/http';

interface UserCreationSearchParams {
username: string;
}

type Schema = HttpSchema<{
'/users': {
POST: {
request: {
headers?: { 'content-type'?: 'application/x-www-form-urlencoded' };
body: HttpSearchParams<UserCreationSearchParams>;
};
};
};
}>;

Then, use the body option to send the data in your fetch request. The HttpSearchParams type will be automatically serialized to a URL-encoded string, and the content-type header will be set to application/x-www-form-urlencoded.

import { HttpSearchParams } from '@zimic/http';
import { createFetch } from '@zimic/fetch';

const fetch = createFetch<Schema>({
baseURL: 'http://localhost:3000',
});

const searchParams = new HttpSearchParams<UserCreationSearchParams>({
username: 'me',
});

const response = await fetch('/users', {
method: 'POST',
body: searchParams,
});

Using response bodies​

Response bodies are declared in your schema using the response.<status>.body property.

JSON response body​

To receive a JSON response body, declare its type in your schema.

schema.ts
import { HttpSchema } from '@zimic/http';

interface User {
id: string;
username: string;
}

type Schema = HttpSchema<{
'/users/:userId': {
GET: {
response: {
200: {
body: User;
};
};
};
};
}>;

Then, use response.json() to parse the response body as JSON. The result is automatically typed according to your schema.

import { createFetch } from '@zimic/fetch';

const fetch = createFetch<Schema>({
baseURL: 'http://localhost:3000',
});

const response = await fetch('/users/1', {
method: 'GET',
});

const user = await response.json();

FormData response body​

To receive a FormData response body, declare its type in your schema. Use the HttpFormData to indicate that the body is a FormData type.

schema.ts
import { HttpFormData, HttpSchema } from '@zimic/http';

interface AvatarFormDataSchema {
image: File;
}

type Schema = HttpSchema<{
'/users/:userId/avatar': {
GET: {
response: {
200: {
headers?: { 'content-type'?: 'multipart/form-data' };
body: HttpFormData<AvatarFormDataSchema>;
};
};
};
};
}>;

Then, use response.formData() to parse the response body as FormData. The result is automatically typed according to your schema.

import { createFetch } from '@zimic/fetch';

const fetch = createFetch<Schema>({
baseURL: 'http://localhost:3000',
});

const response = await fetch(`/users/${user.id}/avatar`, {
method: 'GET',
});

const formData = await response.formData();

Binary response body​

To receive a binary response body, declare its type in your schema.

schema.ts
import { HttpSchema } from '@zimic/http';

interface Video {
id: string;
url: string;
}

type Schema = HttpSchema<{
'/videos/:videoId': {
GET: {
response: {
200: {
headers?: { 'content-type'?: string };
body: Blob;
};
};
};
};
}>;

Then, use response.blob() or response.arrayBuffer() to parse the response body as binary data.

import { createFetch } from '@zimic/fetch';

const fetch = createFetch<Schema>({
baseURL: 'http://localhost:3000',
});

const response = await fetch(`/videos/${video.id}`, {
method: 'GET',
});

const videoBlob = await response.blob();

Response streaming​

Similarly to request streaming, you can stream the response body using ReadableStream. This allows you to receive large files or chunked data in smaller parts, which can reduce memory usage and improve performance.

schema.ts
import { HttpSchema } from '@zimic/http';

type Schema = HttpSchema<{
'/videos/:videoId': {
GET: {
response: {
200: {
headers?: { 'content-type'?: string };
body: ReadableStream;
};
};
};
};
}>;
import fs from 'fs';
import stream, { Readable } from 'stream';
import { ReadableStream as NodeReadableStream } from 'stream/web';

const response = await fetch(`/videos/${video.id}`, {
method: 'GET',
});

const responseStream = Readable.fromWeb(response.body as NodeReadableStream);
const fileStream = fs.createWriteStream('video.mp4');

// Stream the response body to a local file
await stream.promises.pipeline(responseStream, fileStream);

Plain-text response body​

To receive a plain-text response body, declare its type in your schema.

schema.ts
import { HttpSchema } from '@zimic/http';
type Schema = HttpSchema<{
'/content': {
GET: {
response: {
200: {
body: string;
};
};
};
};
}>;

Then, use response.text() to parse the response body as plain text.

import { createFetch } from '@zimic/fetch';

const fetch = createFetch<Schema>({
baseURL: 'http://localhost:3000',
});

const response = await fetch('/content', {
method: 'GET',
});

const content = await response.text();

URL-encoded response body​

To receive a URL-encoded response body, declare its type in your schema. Use the HttpSearchParams to indicate that the body is a URL-encoded type.

schema.ts
import { HttpSchema, HttpSearchParams } from '@zimic/http';

type Schema = HttpSchema<{
'/users': {
GET: {
response: {
200: {
headers?: { 'content-type'?: 'application/x-www-form-urlencoded' };
body: HttpSearchParams<{ username: string }>;
};
};
};
};
}>;

Then, use response.formData() to parse the response body as URL-encoded data. The result is automatically typed according to your schema.

import { createFetch } from '@zimic/fetch';

const fetch = createFetch<Schema>({
baseURL: 'http://localhost:3000',
});

const response = await fetch('/users', {
method: 'GET',
});

const searchParams = await response.formData();