Bff
A proxy to help your team adopt the Backend for Frontend (BFF) cloud pattern
Install / Use
/learn @imranismail/BffREADME
Motivation
The bff (Backend for Frontend) proxy was built out of the requirement to have an API-aware proxy that is capable of routing, filtering, verifying and modifing HTTP request and response.
It is built on top of Google's Martian proxy framework, therefore bff supports the same built-in modifiers as martian does by default.
In addition to that, it provides a modifier to fetch and aggregate remote resources. Additionally, modifiers to merge and patch request and responses are provided out of the box.
These modifiers can be composed together to solve most use-cases that a BFF service may need.
You can learn more about the BFF cloud pattern here:
- https://www.thoughtworks.com/insights/blog/bff-soundcloud
- https://samnewman.io/patterns/architectural/bff/
Install
Executable
# make a temp directory
cd $(mktemp -d)
# download the bff executable into it
curl -sfL https://github.com/imranismail/bff/releases/download/v0.6.3/bff_0.6.3_Linux_x86_64.tar.gz | tar xvz
# move it into $PATH dir
mv bff /usr/local/bin
# test it
bff --help
Container
Supported Flags
-c, --config string config file (default is $XDG_CONFIG_HOME/bff/config.yaml)
-h, --help help for bff
-i, --insecure Skip TLS verify
-p, --port string Port to run the server on (default "5000")
-u, --url string Proxy url
-v, --verbosity int Verbosity
Usage
The proxy is configured with a YAML configuration file. The file path can be set using the --config flag, it defaults to $XDG_CONFIG_HOME/bff/config.yaml
Executable
bff <<YAML
# fetch resources concurrently
- body.MultiFetcher:
resources:
- body.JSONResource:
method: GET
url: https://jsonplaceholder.typicode.com/users/1
behavior: replace # replaces upstream http response
modifier:
status.Verifier:
statusCode: 200 # verify that the resource returns 200 status code
- body.JSONResource:
method: GET
url: https://jsonplaceholder.typicode.com/users/1/todos
behavior: merge # merge with the previous resource
group: todos # group this response into "todos" key
modifier:
status.Verifier:
statusCode: 200 # verify that the resource returns 200 status code
- body.JSONPatch:
scope: [response]
patch:
- {op: move, from: /todos, path: /Todos}
YAML
Container
# make a temp directory
cd $(mktemp -d)
# create config file
cat > config.yml <<EOF
modifiers: |-
# fetch resources concurrently
- body.MultiFetcher:
resources:
- body.JSONResource:
method: GET
url: https://jsonplaceholder.typicode.com/users/1
behavior: replace # replaces upstream http response
modifier:
status.Verifier:
statusCode: 200 # verify that the resource returns 200 status code
- body.JSONResource:
method: GET
url: https://jsonplaceholder.typicode.com/users/1/todos
behavior: merge # merge with the previous resource
group: todos # group this response into "todos" key
modifier:
status.Verifier:
statusCode: 200 # verify that the resource returns 200 status code
- body.JSONPatch:
scope: [response]
patch:
- {op: move, from: /todos, path: /Todos}
EOF
# run it
docker run --rm -it -v $(pwd)/config.yml:/srv/config.yml ghcr.io/imranismail/bff:latest
Config Reference
config.yml
# env: BFF_INSECURE
# flag: --insecure -i
# type: bool
# required: false
# default: false
insecure: false
# env: BFF_PORT
# flag: -p --port
# type: int
# required: false
# default: 5000
port: 5000
# env: BFF_URL
# flag: -u --url
# type: string
# required: false
url: ""
# env: BFF_VERBOSITY
# flag: -v --verbosity
# type: int
# required: false
# default: 1
# options:
# - panic 5
# - fatal 4
# - error 3
# - warn 2
# - info 1
# - debug 0
# - trace -1
verbosity: 1
# env: BFF_MODIFIERS
# flag: N/A instead it can be set via linux pipe. example: `cat modifiers.yaml | bff` or `bff <<EOF ...config EOF`
# type: string
# required: false
# default: ""
modifiers: |
- body.MultiFetcher:
resources:
- body.JSONResource:
method: GET
url: https://jsonplaceholder.typicode.com/users/1
behavior: replace # replaces upstream http response
modifier:
status.Verifier:
statusCode: 200 # verify that the resource returns 200 status code
- body.JSONResource:
method: GET
url: https://jsonplaceholder.typicode.com/users/1/todos
behavior: merge # merge with the previous resource
group: todos # group this response into "todos" key
modifier:
status.Verifier:
statusCode: 200 # verify that the resource returns 200 status code
- body.JSONPatch:
scope: [response]
patch:
- {op: move, from: /todos, path: /Todos}
Modifiers
This reference is adapted from Martian's wiki
Modifiers are able to mutate a request, a response or both.
JSONResource
The body.JSONResource fetches a remote JSON resource and merges/replaces the upstream response body with the response of the remote request depending on the behavior option. Defaults to the merge behavior.
The merging is done using RFC7386: JSON Merge Patch
body.JSONResource:
scope: [response]
method: GET
url: https://jsonplaceholder.typicode.com/users/1
behavior: replace # replaces upstream http response
allowedHeaders: ["Authorization"] # allow downstream req headers
modifier:
status.Verifier:
statusCode: 200
JSONPatch
The body.JSONPatch patches the JSON request or response body using RFC6902: JSON Patch
body.JSONPatch:
scope: [response]
patch:
- { op: move, from: /todos, path: /Todos }
- { op: add, path: /foo, value: ":foo" } # substitution using values extracted from bff.URLFilter
JSONMapPatch
The body.JSONMapPatch is like body.JSONPatch except that it applies the patch over a collection.
body.JSONMapPatch:
scope: [response]
path: /array # optional, defaults to /
patch:
- { op: move, from: /todos, path: /Todos }
- { op: add, path: /foo, value: ":foo" } # substitution using values extracted from bff.URLFilter
Method
The bff.MethodModifier will modify the HTTP method, supported options are listed here https://go.googlesource.com/go/+/go1.16.2/src/net/http/method.go#10
bff.MethodModifier:
scope: [request]
method: POST
Skip
The skip.RoundTrip skips the HTTP roundtrip to the upstream URL that was specified via the --url flag
skip.RoundTrip:
scope: [request]
Cookie
The cookie.Modifier injects a cookie into a request or a response.
Example configuration that injects a Martian-Cookie cookie into responses:
cookie.Modifier:
scope: [response]
name: Martian-Cookie
value: some value
path: "/some/path"
domain: example.com
expires: "2025-04-12T23:20:50.52Z" # RFC 3339
secure: true
httpOnly: false
maxAge: 86400
Header
The header.Modifier injects or modifies headers in a request or a response.
Example configuration that injects an X-Martian header with the value of
true into requests:
header.Modifier:
scope: [request]
name: X-Martian
value: "true"
Header Blacklist
The header.Blacklist deletes headers from a request or a response.
Example configuration that deletes response headers with the names
X-Martian and Y-Martian:
header.Blacklist:
scope: [response]
names: [X-Martian, Y-Martian]
Query String
The querystring.Modifier adds or modifies query string parameters on a
request. Any existing parameter values are replaced.
Example configuration that sets the query string parameter foo to the value
of bar on requests and responses:
querystring.Modifier:
scope: [request, response]
name: foo
value: bar
The additional bff.querystringModifier allows copying and renaming the parameters.
Example configuration that copies the value of foo to fuu and rename the field
bar to baz:
bff.QueryStringModifier
scope: [request]
op: copy
name: foo
value: fuu
bff.QueryStringModifier
scope: [request]
op: move
name: bar
value: baz
Status
The status.Modifier modifies the HTTP status code on a response.
Example configuration that sets the HTTP status of responses to 200:
status.Modifier:
scope: [response]
statusCode: 200
URL
The url.Modifier modifies the URL on a request.
Example configuration that redirects requests to https://www.google.com/proxy?testing=true
url.Modifier:
scope: [request]
scheme: https
host: www.google.com
path: "/proxy"
query: testing=true
Message Body
The body.Modifier modifies the body of a request or response. Additionally, it will modify the following headers to ensure proper transport: Content-Type, Content-Length, Content-Encoding. The body is expected to be uncompressed and Base64 encoded.
body.Modifier:
scope: [request, response]
contentType: text/plain; charset=utf-8
body: TWFydGlhbiBpcyBhd2Vzb21lIQ==
Groups
Groups hold lists of modifiers (or filters, or groups) that are executed in a particular order.
MultiFetcher
A body.MultiFetcher holds a list o
