---
head:
  - - meta
    - name: description
      content: Discover response filters provided by Ts.ED framework. Learn how to transform and customize API responses using classes to enhance your application's data flow.
  - - meta
    - name: keywords
      content: response filter ts.ed express typescript node.js javascript decorators jsonschema class models
---
# Response Filter

Ts.ED response filter provide a [ResponseFilter](/ai/api/platform/platform-response-filter/types/decorators/decorator-response-filter.md) decorator to decorate a class and handle data returned by the endpoint before sending it to your consumer.
The decorator take a `Content-Type` value to define when the class must be used to transform data to the expected result.

It's the right place to implement your own serialization logic. For example, you can define a Response filter to transform data to an XML content or wrap the data into another generic object.

With this decorator, you are able to define multiple Response Filter (but only by Content-Type). Ts.ED will choose the better
Response Filter according to the `Accept` HTTP header on the request object, when present, or fallback to the default Response filter.

## Xml Filter

By using the appropriate `Content-Type`, you can define a Xml serializer as following:

::: code-group

```typescript [XmlResponseFilter.ts]
import {ResponseFilter, ResponseFilterMethods} from "@tsed/platform-response-filter";
import {Context} from "@tsed/platform-params";

@ResponseFilter("text/xml")
export class XmlResponseFilter implements ResponseFilterMethods {
  transform(data: any, ctx: Context) {
    return jsToXML(data);
  }
}
```

```typescript [UserCtrl.ts]
import {Configuration} from "@tsed/di";
import {Returns} from "@tsed/schema";

@Controller("/users")
export class UsersCtrl {
  @Get("/:id")
  @(Returns(200, User).ContentType("application/json"))
  @(Returns(200, String).ContentType("text/xml"))
  async getUser(@PathParams("id") id: string) {
    return new User({id});
  }
}
```

```typescript [Server.ts]
import {Configuration} from "@tsed/di";
import {XmlResponseFilter} from "./filters/XmlResponseFilter.js";

@Configuration({
  responseFilters: [
    XmlResponseFilter
  ]
})
```

```typescript [UsersCtrl.spec.ts]
import {PlatformTest} from "@tsed/platform-http/testing";
import * as SuperTest from "supertest";
import {UsersCtrl} from "./UsersCtrl.js";
import {Server} from "../../Server.js";

describe("UserCtrl", () => {
  let request: SuperTest.Agent;

  before(
    PlatformTest.bootstrap(Server, {
      mount: {
        "/rest": [UsersCtrl]
      },
      responseFilters: [XmlResponseFilter]
    })
  );
  before(() => {
    request = SuperTest(PlatformTest.callback());
  });
  after(PlatformTest.reset);
  it("should return the xml format", async () => {
    const response = await request
      .get("/rest/users/1")
      .set({
        Accept: "text/xml"
      })
      .expect(200);

    expect(response.text).toEqual("<xml>...</xml>");
  });
  it("should return the default format", async () => {
    const response = await request.get("/rest/users/1").expect(200);

    expect(response.body).toEqual({id: "1"});
  });
});
```

:::

::: warning
Don't forget to register your Response Filter by adding your class to `responseFilters` field on the server configuration.
:::

## Wrap responses

One of the usage of the Response Filter could be to wrap all returned data into a generic object.
To doing that, use the `application/json` Content-Type with the [ResponseFilter](/ai/api/platform/platform-response-filter/types/decorators/decorator-response-filter.md) decorator
to wrap data to the expected result:

::: code-group

```typescript [WrapperResponseFilter.ts]
import {ResponseFilter, ResponseFilterMethods} from "@tsed/platform-response-filter";
import {Context} from "@tsed/platform-params";

@ResponseFilter("application/json")
export class WrapperResponseFilter implements ResponseFilterMethods {
  transform(data: any, ctx: Context) {
    return {data, errors: [], links: []};
  }
}
```

```typescript [UserCtrl.ts]
import {Configuration} from "@tsed/di";
import {Returns} from "@tsed/schema";

@Controller("/users")
export class UsersCtrl {
  @Get("/:id")
  @(Returns(200, User).ContentType("application/json"))
  @(Returns(200, String).ContentType("text/xml"))
  async getUser(@PathParams("id") id: string) {
    return new User({id});
  }
}
```

```typescript [Server.ts]
import {Configuration} from "@tsed/di";
import {WrapperResponseFilter} from "./filters/WrapperResponseFilter";

@Configuration({
  responseFilters: [
    WrapperResponseFilter
  ]
})
```

```typescript [UsersCtrl.spec.ts]
import {PlatformTest} from "@tsed/platform-http/testing";
import * as SuperTest from "supertest";
import {UsersCtrl} from "./UsersCtrl.js";
import {Server} from "../../Server.js";

describe("UserCtrl", () => {
  let request: SuperTest.Agent;

  before(
    PlatformTest.bootstrap(Server, {
      mount: {
        "/rest": [UsersCtrl]
      },
      responseFilters: [XmlResponseFilter]
    })
  );
  before(() => {
    request = SuperTest(PlatformTest.callback());
  });
  after(PlatformTest.reset);
  it("should return the wrapped data", async () => {
    const response = await request.get("/rest/users/1").expect(200);

    expect(response.body).toEqual({data: {id: "1"}, errors: [], links: []});
  });
});
```

::: warning
The wrapper won't be documented in your generated `swagger.json`!
:::

## Handle all responses

By using the `*/*` Content-Type value given to the [ResponseFilter](/ai/api/platform/platform-response-filter/types/decorators/decorator-response-filter.md) you can intercept all data.

```typescript
import {ResponseFilter, ResponseFilterMethods} from "@tsed/platform-response-filter";
import {Context} from "@tsed/platform-params";

@ResponseFilter("*/*")
export class AnyResponseFilter implements ResponseFilterMethods {
  transform(data: any, ctx: Context) {
    // do something
    return data;
  }
}
```

## Pagination

The following advanced example will show you how you can combine the different Ts.ED features to describe Pagination.
The used features are the following:

-   [Generics](/docs/model#generics)
-   [Function programming to declare models](/docs/model#using-functions)
-   [For](/ai/api/specs/schema/types/decorators/common/decorator-for.md) decorator to declare a custom model for JsonSchema, OS2 or OS3.
-   Response Filter to manage paginated response.

::: code-group

```ts [ProductsCtrl.ts]
import {QueryParams} from "@tsed/platform-params";
import {Get, Returns} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {Pageable} from "../models/Pageable";
import {Pagination} from "../models/Pagination";
import {Product} from "../models/Product";

@Controller("/pageable")
class ProductsCtrl {
  @Get("/")
  @Returns(206, Pagination).Of(Product).Title("PaginatedProduct")
  @Returns(200, Pagination).Of(Product).Title("PaginatedProduct")
  get(@QueryParams() pageableOptions: Pageable, @QueryParams("all") all: boolean) {
    return new Pagination<Product>({
      data: [
        new Product({
          id: "100",
          title: "CANON D3000"
        })
      ],
      totalCount: all ? 1 : 100, // just for test,
      pageable: pageableOptions
    });
  }
}
```

```ts [Pageable.ts]
import {isString} from "@tsed/core";
import {OnDeserialize} from "@tsed/json-mapper";
import {array, Default, Description, For, Integer, Min, oneOf, SpecTypes, string} from "@tsed/schema";

class Pageable {
  @Integer()
  @Min(0)
  @Default(0)
  @Description("Page number.")
  page: number = 0;

  @Integer()
  @Min(1)
  @Default(20)
  @Description("Number of objects per page.")
  size: number = 20;

  @For(SpecTypes.JSON, oneOf(string(), array().items(string()).maxItems(2)))
  @For(SpecTypes.OPENAPI, array().items(string()).maxItems(2))
  @OnDeserialize((value: string | string[]) => (isString(value) ? value.split(",") : value))
  @Description(
    "Sorting criteria: property(,asc|desc). Default sort order is ascending. Multiple sort criteria are supported."
  )
  sort: string | string[];

  constructor(options: Partial<Pageable>) {
    options.page && (this.page = options.page);
    options.size && (this.size = options.size);
    options.sort && (this.sort = options.sort);
  }

  get offset() {
    return this.page ? this.page * this.limit : 0;
  }

  get limit() {
    return this.size;
  }
}
```

```ts [Pagination.ts]
import {CollectionOf, Default, Generics, Integer, MinLength} from "@tsed/schema";
import {Pageable} from "./Pageable";

export class PaginationLink {
  @Property()
  next: string;

  @Property()
  prev: string;
}

@Generics("T")
export class Pagination<T> extends Pageable {
  @CollectionOf("T")
  data: T[];

  @Integer()
  @MinLength(0)
  @Default(0)
  totalCount: number = 0;

  @Property()
  links: PaginationLink = new PaginationLink();

  constructor({data, totalCount, pageable}: Partial<Pagination<T>> & {pageable: Pageable}) {
    super(pageable);
    data && (this.data = data);
    totalCount && (this.totalCount = totalCount);
  }
}
```

```ts [Product.ts]
import {Property} from "@tsed/schema";

class Product {
  @Property()
  id: string;

  @Property()
  title: string;

  constructor({id, title}: Partial<Product> = {}) {
    id && (this.id = id);
    title && (this.title = title);
  }
}
```

```ts [PaginationFilter.ts]
import {PlatformContext} from "@tsed/platform-http";
import {ResponseFilter, ResponseFilterMethods} from "@tsed/platform-response-filter";
import {Pagination} from "../models/Pagination";

@ResponseFilter("application/json")
class PaginationFilter implements ResponseFilterMethods {
  transform(data: unknown, ctx: PlatformContext): any {
    if (ctx.data instanceof Pagination) {
      // /!\ don't modify the ctx.data. at this step, the serializer has already been called.

      if (ctx.data.totalCount > (ctx.data.pageable.page + 1) * ctx.data.pageable.size) {
        ctx.response.status(206);
        data.links.next = `${ctx.request.url}?page=${ctx.data.pageable.page + 1}&size=${ctx.data.pageable.size}`;
      }

      if (ctx.data.pageable.page > 0) {
        data.links.prev = `${ctx.request.url}?page=${ctx.data.pageable.page - 1}&size=${ctx.data.pageable.size}`;
      }
    }

    return data;
  }
}
```

```ts [ProductsCtrl.spec.ts]
import {Property} from "@tsed/schema";

class Product {
  @Property()
  id: string;

  @Property()
  title: string;

  constructor({id, title}: Partial<Product> = {}) {
    id && (this.id = id);
    title && (this.title = title);
  }
}
```

:::
