Skip to main content

Operators

Tailcall DSL builds on your existing GraphQL knowledge by allowing the addition of some custom operators. These operators provide powerful compile time guarantees to make sure your API composition is tight and robust. The operator information is used to automatically generates highly optimized resolver logic for your types.

@server

The @server directive, when applied at the schema level, offers a comprehensive set of server configurations. It dictates how the server behaves and helps tune tailcall for various use-cases.

schema @server(...[ServerSettings]...){
query: Query
mutation: Mutation
}

In this templated structure, replace ...[ServerSettings]... with specific configurations tailored to your project's needs. Adjust and expand these settings as necessary.

The various ServerSettings options and their details are explained below.

workers

workers sets the number of worker threads the server will use. If not specified, the default value is the number of cores available to the system.

schema @server(workers: 32) {
query: Query
mutation: Mutation
}

In this example, the workers is set to 32. This means that the Tailcall server will use 32 worker threads.

port

This refers to the port on which the Tailcall will be running. If not specified, the default port is 8000.

schema @server(port: 8090) {
query: Query
mutation: Mutation
}

In this example, the port is set to 8090. This means that the Tailcall will be accessible at http://localhost:8090.

tip

Always lean towards non-standard ports, steering clear of typical ones like 80 or 8080. Ensure your chosen port is unoccupied.

cacheControlHeader

The cacheControlHeader configuration, when activated, instructs Tailcall to transmit Cache-Control headers in its responses. The max-age value in the header, is the least of the values in the responses received by tailcall from the upstream services. By default, this is set to false meaning no header is set.

schema @server(cacheControlHeader: true) {
query: Query
mutation: Mutation
}

graphiql

The grahiql configuration enables the GraphiQL IDE at the root (/) path within Tailcall. GraphiQL is a built-in, interactive in-browser GraphQL IDE, designed to streamline query development and testing. By default, this feature is turned off.

schema @server(port: 8000, graphiql: true) {
query: Query
mutation: Mutation
}
tip

While the GraphiQL interface is a powerful tool for development, it's recommended to disable it in production environments, especially if you're not exposing GraphQL APIs directly to users. This ensures an added layer of security and reduces unnecessary exposure.

vars

This configuration allows you to define local variables that can be leveraged during the server's operations. These variables are particularly handy when you need to store constant configurations, secrets, or other shared information that various operations might require.

schema @server(vars: {key: "apiKey", value: "YOUR_API_KEY_HERE"}) {
query: Query
mutation: Mutation
}

type Query {
externalData: Data
@http(path: "/external-api/data", headers: [{key: "Authorization", value: "Bearer {{vars.apiKey}}"}])
}

In the provided example, a variable named apiKey is set with a placeholder value of "YOUR_API_KEY_HERE". This configuration implies that whenever Tailcall fetches data from the externalData endpoint, it includes the apiKey in the Authorization header of the HTTP request.

tip

Local variables, like apiKey, can be instrumental in securing access to external services or providing a unified place for configurations. Ensure that sensitive information stored this way is well protected and not exposed unintentionally, especially if your Tailcall configuration is publicly accessible.

introspection

This setting governs whether introspection queries are permitted on the server. Introspection is an intrinsic feature of GraphQL, allowing clients to fetch information about the schema directly. This can be instrumental for tools and client applications to understand the types, fields, and operations available. By default, this setting is enabled (true).

schema @server(introspection: false) {
query: Query
mutation: Mutation
}
tip

Although introspection is beneficial during development and debugging stages, it's wise to consider disabling it in production environments. Turning off introspection in live deployments can enhance security by preventing potential attackers from easily discerning the schema and any associated business logic or data structures.

queryValidation

The queryValidation configuration specifies whether the server should validate incoming GraphQL queries against the defined schema. Validating each query ensures its conformity to the schema, preventing errors from invalid or malformed queries. However, there are situations where you might opt to disable it, notably when seeking to enhance server performance at the cost of such checks. This defaults to false if not specified.

schema @server(queryValidation: true) {
query: Query
mutation: Mutation
}

In the example above, queryValidation is set to true, enabling the validation phase for incoming queries.

tip

This should be enabled in dev environment to make sure the queries sent are correct and validated, however in production env, you could consider disabling it for improved performance.

responseValidation

Tailcall automatically can infer the schema of the http endpoints for you. This information can be used to validate responses that are received from the upstream services. Enabling this setting allows you to perform exactly that. If this is not specified, the default setting for responseValidation is false.

schema @server(responseValidation: true) {
query: Query
mutation: Mutation
}
tip

Disabling this setting will offer major performance improvements, but at the potential expense of data.

globalResponseTimeout

The globalResponseTimeout configuration determines the maximum duration a query is allowed to run before it's terminated by the server. Essentially, it acts as a safeguard against long-running queries that could strain resources or pose security concerns.

If not explicitly defined, there might be a system-specific or default value that applies.

schema @server(globalResponseTimeout: 5000) {
query: Query
mutation: Mutation
}

In this given example, the globalResponseTimeout is set to 5000 milliseconds, or 5 seconds. This means any query execution taking longer than this duration will be automatically terminated by the server.

tip

It's crucial to set an appropriate response timeout, especially in production environments. This not only optimizes resource utilization but also acts as a security measure against potential denial-of-service attacks where adversaries might run complex queries to exhaust server resources.

http

The version of HTTP to be used by the server. If not specified, the default value is HTTP1. The available options are HTTP1 and HTTP2.

schema @server(http: HTTP2) {
query: Query
mutation: Mutation
}

cert

The path to certificate(s) to be used when running the server over HTTP2 (HTTPS). If not specified, the default value is null.

schema @server(cert: "./cert.pem") {
query: Query
mutation: Mutation
}
tip

The certificate can be of any extension, but it's highly recommended to use standards (pem, crt, key).

key

The path to key to be used when running the server over HTTP2 (HTTPS). If not specified, the default value is null.

schema @server(key: "./key.pem") {
query: Query
mutation: Mutation
}
tip

The key can be of any extension, but it's highly recommended to use standards (pem, crt, key).

@upstream

The upstream directive allows you to control various aspects of the upstream server connection. This includes settings like connection timeouts, keep-alive intervals, and more. If not specified, default values are used.

schema @upstream(...[UpstreamSetting]...){
query: Query
mutation: Mutation
}

The various UpstreamSetting options and their details are explained below.

poolIdleTimeout

The time in seconds that the connection pool will wait before closing idle connections.

schema @upstream(poolIdleTimeout: 60, baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
mutation: Mutation
}

poolMaxIdlePerHost

The maximum number of idle connections that will be maintained per host.

schema @upstream(poolMaxIdlePerHost: 60, baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
mutation: Mutation
}

keepAliveInterval

The time in seconds between each keep-alive message sent to maintain the connection.

schema @upstream(keepAliveInterval: 60, baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
mutation: Mutation
}

keepAliveTimeout

The time in seconds that the connection will wait for a keep-alive message before closing.

schema @upstream(keepAliveTimeout: 60, baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
mutation: Mutation
}

keepAliveWhileIdle

A boolean value that determines whether keep-alive messages should be sent while the connection is idle.

schema @upstream(keepAliveWhileIdle: false, baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
mutation: Mutation
}

proxy

The proxy setting defines an intermediary server through which the upstream requests will be routed before reaching their intended endpoint. By specifying a proxy URL, you introduce an additional layer, enabling custom routing and security policies.

schema @upstream(proxy: {url: "http://localhost:3000"}, baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
mutation: Mutation
}

In the provided example, we've set the proxy's url to "http://localhost:3000". This configuration ensures that all requests aimed at the designated baseURL are first channeled through this proxy. To illustrate, if the baseURL is "http://jsonplaceholder.typicode.com", any request targeting it would be initially sent to "http://localhost:3000" before being redirected to its final destination.

connectTimeout

The time in seconds that the connection will wait for a response before timing out.

schema @upstream(connectTimeout: 60, baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
mutation: Mutation
}

timeout

The maximum time in seconds that the connection will wait for a response.

schema @upstream(timeout: 60, baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
mutation: Mutation
}

tcpKeepAlive

The time in seconds between each TCP keep-alive message sent to maintain the connection.

schema @upstream(tcpKeepAlive: 60, baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
mutation: Mutation
}

userAgent

The User-Agent header value to be used in HTTP requests.

schema @upstream(userAgent: "Tailcall/1.0", baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
mutation: Mutation
}

allowedHeaders

The allowedHeaders configuration specifies which HTTP headers are permitted to be forwarded to upstream services when making requests. If allowedHeaders isn't specified, no incoming headers will be forwarded to the upstream services, which can provide an added layer of security but might restrict essential data flow.

schema @upstream(allowedHeaders: ["Authorization", "X-Api-Key"]) {
query: Query
mutation: Mutation
}

In the example above, the allowedHeaders is set to allow only Authorization and X-Api-Key headers. This means that requests containing these headers will forward them to upstream services, while all others will be ignored. It ensures that only expected headers are communicated to dependent services, emphasizing security and consistency.

baseURL

This refers to the default base URL for your APIs. If it's not explicitly mentioned in the @upstream operator, then each @http operator must specify its own baseURL. If neither @upstream nor @http provides a baseURL, it results in a compilation error.

schema @upstream(baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
mutation: Mutation
}

In this representation, the baseURL is set as http://jsonplaceholder.typicode.com. Thus, all API calls made by @http will prepend this URL to their respective paths.

tip

Ensure that your base URL remains free from specific path segments.

  • GOOD: @upstream(baseURL: http://jsonplaceholder.typicode.com)
  • BAD: @upstream(baseURL: http://jsonplaceholder.typicode.com/api)

httpCache

When activated, directs Tailcall to utilize HTTP caching mechanisms. These mechanisms, in accordance with the HTTP Caching RFC, are designed to improve performance by reducing unnecessary data fetches. If left unspecified, this feature defaults to false.

schema @upstream(httpCache: false) {
query: Query
mutation: Mutation
}

batchRequests

Batching in GraphQL combines multiple requests into one, reducing server round trips.

schema @server(
port: 8000
batchRequests: true
)

Trade-offs

Batching can improve performance but may introduce latency if one request in the batch takes longer. It also makes network traffic debugging harder.

Tips

  • Only use batching if necessary and other optimization techniques don't resolve performance issues.
  • Use batching judiciously and monitor its impact.
  • Be aware that batching can complicate debugging

batch

An object that specifies the batch settings, including maxSize (the maximum size of the batch), delay (the delay in milliseconds between each batch), and headers (an array of HTTP headers to be included in the batch).

schema @upstream(batch: {maxSize: 1000, delay: 10, headers: ["X-Server", "Authorization"]}) {
query: Query
mutation: Mutation
}

@http

This @http operator serves as an indication of a field or node that is underpinned by a REST API. For Example:

type Query {
user(id: ID!): User @http(path: "/users")
}

In this example, the @http operator is added to the user field of the Query type. This means that the user field is underpinned by a REST API. The path argument is used to specify the path of the REST API. In this case, the path is /users. This means that the GraphQL server will make a GET request to https://jsonplaceholder.typicode.com/users when the user field is queried.

baseURL

This refers to the base URL of the API. If not specified, the default base URL is the one specified in the @server operator.

type Query {
user(id: ID!): User @http(path: "/users", baseURL: "https://jsonplaceholder.typicode.com")
}

path

This refers to the API endpoint you're going to call. For instance https://jsonplaceholder.typicode.com/users`.

type Query {
user(id: ID!): User @http(path: "/users")
}

If your API endpoint contains dynamic segments, you can use Mustache templates to substitute variables. For example, to fetch a specific user, the path can be written as /users/{{args.id}}.

type Query {
user(id: ID!): User @http(path: "/users/{{args.id}}")
}

method

This refers to the HTTP method of the API call. Commonly used methods include GET, POST, PUT, DELETE, etc. If not specified, the default method is GET. For example:

type Mutation {
createUser(input: UserInput!): User @http(method: "POST", path: "/users")
}

query

This represents the query parameters of your API call. You can pass it as a static object or use Mustache template for dynamic parameters. These parameters will be added to the URL. For example:

type Query {
userPosts(id: ID!): [Post] @http(path: "/posts", query: [{key: "userId", value: "{{args.id}}"}])
}

body

The body of the API call. It's used for methods like POST or PUT that send data to the server. You can pass it as a static object or use a Mustache template to substitute variables from the GraphQL variables. For example:

type Mutation {
createUser(input: UserInput!): User @http(method: "POST", path: "/users", body: "{{args.input}}")
}

In the example above, the createUser mutation sends a POST request to /users, with the input object converted to JSON and included in the request body.

headers

The headers parameter allows you to customize the headers of the HTTP request made by the @http operator. It is used by specifying a key-value map of header names and their values.

For instance:

type Mutation {
createUser(input: UserInput!): User @http(path: "/users", headers: [{key: "X-Server", value: "Tailcall"}])
}

In this example, a request to /users will include an additional HTTP header X-Server with the value Tailcall.

You can make use of mustache templates to provide dynamic values for headers, derived from the arguments or context provided in the request. For example:

type Mutation {
users(name: String): User
@http(path: "/users", headers: [{key: "X-Server", value: "Tailcall"}, {key: "User-Name", value: "{{args.name}}"}])
}

In this scenario, the User-Name header's value will dynamically adjust according to the name argument passed in the request.

groupBy

The groupBy parameter groups multiple data requests into a single call. For more details please refer out n + 1 guide.

type Post {
id: Int!
name: String!
user: User @http(path: "/users", query: [{key: "id", value: "{{value.userId}}"}], groupBy: ["id"])
}
  • query: {key: "id", value: "{{value.userId}}"}]: Here, TailCall CLI is instructed to generate a URL where the user id aligns with the userId from the parent Post. For a batch of posts, the CLI compiles a single URL, such as /users?id=1&id=2&id=3...id=10, consolidating multiple requests into one.

@modify

The @modify operator in GraphQL provides the flexibility to alter the attributes of a field or a node within your GraphQL schema. Here's how you can use this operator:

name

You can rename a field or a node in your GraphQL schema using the name argument in the @modify operator. This can be helpful when the field name in your underlying data source doesn't match the desired field name in your schema. For instance:

type User {
id: Int! @modify(name: "userId")
}

@modify(name: "userId") tells GraphQL that although the field is referred to as idin the underlying data source, it should be presented as userId in your schema.

omit

You can exclude a field or a node from your GraphQL schema using the omit argument in the @modify operator. This can be useful if you want to keep certain data hidden from the client. For instance:

type User {
id: Int! @modify(omit: true)
}

@modify(omit: true) tells GraphQL that the id field should not be included in the schema, thus it won't be accessible to the client.

@addField

The @addField operator simplifies data structures and queries by adding a field that inlines or flattens a nested field or node within your schema. It works by modifying the schema and the data transformation process, simplifiying how nested data is accessed and presented.

For instance, consider a schema:

schema {
query: Query
}

type User @addField(name: "street", path: ["address", "street"]) {
id: Int!
name: String!
username: String!
email: String!
phone: String
website: String
address: Address @modify(omit: true)
}

type Address {
street: String!
city: String!
state: String!
}

type Query {
user(id: Int!): User @http(path: "/users/{{args.id}}")
}

Suppose we are only interested in the street field in Address.

The @addField operator above, applied to the User type in this case, creates a field called street in the User type. It includes a path argument, indicating the chain of fields to be traversed from a declared field (address in this case), to the field within Address to be added. We can also add a @modify(omit: true) to omit the address field from the schema, since we have already made its street field available on the User type.

Post application, the schema becomes:

schema {
query: Query
}

type User {
id: Int!
name: String!
username: String!
email: String!
phone: String
website: String
street: String
}

type Query {
user(id: Int): Post!
}

In the above example, since we added a @modify(omit: true) on the address field, the Address type is eliminated from the schema.

The @addField operator also take cares of nullablity of the fields. If any of the fields in the path is nullable, the resulting type will be nullable.

Additionally, @addField supports indexing, meaning you can specify the array index to be inlined. If a field posts is of type [Post], and you want to, for example, get the title of the first post, you can specify the path as ["posts","0","title"].

type User @addField(name: "firstPostTitle", path: ["posts", "0", "title"]) {
id: Int!
name: String!
username: String!
email: String!
phone: String
website: String
posts: Post @http(path: "/users/{{value.id}}/posts")
}

type Post {
id: Int!
userId: Int!
title: String!
body: String!
}

In conclusion, the @addField operator helps tidy up your schema and streamline data fetching by reducing query depth, promoting better performance and simplicity.

@const

The @const operators allows us to embed a constant response for the schema. For eg:

schema {
query: Query
}

type Query {
user: User @const(data: {name: "John", age: 12})
}

type User {
name: String
age: Int
}

The const operator will also validate the provided value at compile time to make sure that it matches the of the field. If the schema of the provided value doesn't match the type of the field, a descriptive error message is show on the console.