A Facility Service Definition (FSD) describes the operations supported by a Facility API.
Unlike other API definition formats, an FSD focuses on the shape of the client library rather than on the HTTP paths. Each service operation has a name, request fields, and response fields. In that way it resembles RPC more than REST, though it can certainly be used to describe RESTful APIs.
You can also use OpenAPI 2.0 (aka Swagger) by using a tool to convert an API in that format to/from an FSD.
A Facility Service Definition is represented by an FSD file.
An FSD file uses a domain-specific language in an effort to make Facility Service Definitions easier to read and write, especially for developers comfortable with C-style languages.
Each FSD file contains the definition of one service. The name of an FSD file is typically the service name with an .fsd
file extension, e.g. MyApi.fsd
.
An FSD file is a text file encoded with UTF-8 and no byte order mark (BOM).
Every service has a name. Unless otherwise noted, a name in this specification must start with an ASCII letter but may otherwise contain ASCII letters, numbers, and/or underscores.
A service contains one or more service elements: methods, data transfer objects, enumerated types, and error sets. A service can also have attributes, a summary, and remarks.
In an FSD file, the service
keyword starts the definition of a service. It is followed by the service name and optionally preceded by service attributes.
The methods and other service items follow the service name, enclosed in braces.
[http(url: "https://api.example.com/v1/")]
[info(version: 2.1.3)]
service MyApi
{
method myMethod { … }: { … }
data MyData { … }
enum MyEnum { … }
…
}
It is also possible to omit the curly braces around the service members and instead use a semicolon after the service name.
[http(url: "https://api.example.com/v1/")]
[info(version: 2.1.3)]
service MyApi;
method myMethod { … }: { … }
data MyData { … }
enum MyEnum { … }
…
The url
parameter of the http
attribute indicates the base URL where the HTTP service lives. The trailing slash is optional. If the attribute or its parameter is omitted, the client will have to provide the base URL.
If desired, use the version
parameter of the info
attribute to indicate the version of the API.
Attributes are used to attach additional information to a service and its elements.
Each attribute has an alphanumeric name and may optionally include one or more parameters. Each parameter has its own name as well as a value.
The obsolete
attribute indicates that the service element is obsolete and/or deprecated and should no longer be used. The optional message
parameter can be used to provide additional information, e.g. what should be used instead.
In an FSD file, the attribute name is surrounded by square brackets. The comma-delimited parameters, if any, are surrounded by parentheses and follow the attribute name.
Each parameter value can be represented as an ASCII token or a JSON-style double-quoted string. An ASCII token can consist of numbers, letters, periods, hyphens, plus signs, and/or underscores. An ASCII token is not semantically different than a double-quoted string containing that token.
[obsolete] // no parameters
[csharp(name: query)] // one parameter
[http(path: "/search", code: 202)] // two parameters
Multiple attributes can be comma-delimited within one set of square brackets (as below) and/or specified in separate sets of square brackets (as above).
[obsolete, csharp(name: query)]
Every Facility API has a default HTTP mapping. The HTTP mapping can be customized by using the http
attribute, which can be applied to services, methods, request fields, response fields, and errors.
The http
attribute is always optional. When the attribute is omitted, the defaults are used, as documented.
Each method represents an operation of the service.
A method has a name, request fields, and response fields. A method can also have attributes, a summary, and remarks.
When a client invokes a service method, it provides values for some or all of the request fields. For example, a translation service could have a translate
method with request fields named text
, sourceLanguage
, and targetLanguage
.
If the method succeeds, values are returned for some or all of the response fields. A translate
method might return the translated text in a text
field and a confidence level in a confidence
field.
If the method fails, a service error is returned instead.
In an FSD file, the method
keyword starts the definition of a method. It is followed by the method name and optionally preceded by method attributes.
The request and response follow the method name, each enclosed in braces and separated by a colon (:
). The request and response fields, if any, are listed within the braces.
method translate
{
text: string;
sourceLanguage: string;
targetLanguage: string;
}:
{
text: string;
confidence: double;
}
The http
attribute of a method supports a parameter named method
that indicates the HTTP method that is used, e.g. GET
, POST
, PUT
, DELETE
, or PATCH
. If omitted, the default is POST
. Lowercase is permitted, e.g. [http(method: get)]
.
The path
parameter indicates the HTTP path of the method (relative to the base URL). The path must start with a slash. A single slash indicates that the method is at the base URL itself.
For example, if a method uses [http(method: GET, path: "/widgets"]
in a service that uses [http(url: "https://api.example.com/v1/"]
, the full HTTP method and path for that method would be GET https://api.example.com/v1/widgets
.
If the path
parameter is not specified, it defaults to the method name, e.g. /getWidgets
for a method named getWidgets
. This default would not be appropriate for a RESTful API, but may be acceptable for an RPC-style API.
The code
parameter indicates the HTTP status code used if the method is successful (but see also body fields below). If omitted, it defaults to 200
(OK). If code
is set to 204
(No Content) or 304
(Not Modified), no content is returned and normal fields are prohibited.
[http(method: POST, path: "/jobs/start", code: 202)]
method startJob
{
…
}:
{
…
}
A field stores data for a method request, method response, or data transfer object.
Each field has a name and a type. The field type restricts the type of data that can be stored in that field.
Fields are generally optional, i.e. they may or may not store a value.
The following primitive field types are supported:
string
: A string of zero or more Unicode characters.datetime
: An ISO 8601 UTC datetime string with an uppercase T
, Z
, no fractional seconds, and no time offset.boolean
: A Boolean value, i.e. true or false.double
: A double-precision floating-point number.int32
: A 32-bit signed integer.int64
: A 64-bit signed integer.decimal
: A 128-bit number appropriate for monetary calculations.datetime
: A date (Gregorian year, month, and day) and time (UTC hour, minute, and second).bytes
: Zero or more bytes.object
: An arbitrary JSON object.error
: A service error.A field type can be any data transfer object or enumerated type in the service, referenced by name.
A field type can be an array, i.e. zero or more ordered items of a particular type, including primitive types, data transfer objects, enumerated types, or service results. Use T[]
to indicate an array; for example, int32[]
is an array of int32
.
A field type can be a dictionary that maps strings to values of a particular type, including primitive types, data transfer objects, enumerated types, or service results. Use map<T>
to indicate a map; for example, map<int32>
is a map of strings to int32
.
A field type can be a service result. Use result<T>
to indicate a service result; for example, result<Widget>
is a service result whose value is a DTO named Widget
.
A field type can be nullable, i.e. can distinguish being unspecified from being explicitly null. (Normally, null is not permitted, or is treated the same as unspecified.) Use nullable<T>
to indicate a nullable type; for example, a nullable<bool>
field can be unspecified, null, true, or false.
In an FSD file, a field is represented by a name and a field type, which are separated by a colon (:
) and followed by a semicolon (;
). Fields can also be preceded by field attributes.
For fields whose type corresponds to a DTO or enumerated type, that DTO or enumerated type must be defined elsewhere in this service, before or after the method or DTO that contains the field.
Since JSON is currently the most commonly-used serialization format for APIs over HTTP, Facility APIs are designed to be trivially compatible with JSON.
In a JSON request body, response body, or DTO, a field is serialized as a JSON object property. In fact, to avoid complicating implementations, there is no way to customize the JSON serialization of a request body, response body, or DTO. Each field is always serialized as a JSON property with the same name.
string
, boolean
, double
, int32
, int64
, and decimal
are encoded as JSON literals.datetime
is encoded as a date-time
string as specified by RFC 3339 §5.6, e.g. 2023-08-10T16:15:43Z
. It must use an uppercase T
, an uppercase Z
, no fractional sections, and no time offset. APIs that need more flexibility should use a string
instead.bytes
are encoded as a Base64 string.object
is encoded as a JSON object.error
is encoded as a JSON object with code
, message
, details
, and innerError
properties.result<T>
is encoded as a JSON object with a value
or error
property.T[]
is encoded as a JSON array.map<T>
is encoded as a JSON object.nullable<T>
is encoded as an explicit null
when null.If a JSON property is set to null
, it is treated as though it were omitted, unless the field type is nullable<T>
. Arrays and maps are not permitted to have null
items, unless they are arrays or maps of nullable<T>
.
Even though int64
is a 64-bit signed integer, avoid ±251 or larger, as JavaScript cannot safely represent integers that large. Similarly, JavaScript numbers cannot accurately represent decimal
.
When reading JSON, conforming clients and servers may match property names case-insensitively, and they may perform type conversions for property values, e.g. strings to numbers. They may also support non-standard JSON features, such as unquoted property names, single-quoted strings, comments, etc.
However, when writing JSON, conforming clients and servers must always use standard JSON with no comments, correctly-cased property names, correctly-typed property values, etc.
Validation requirements for fields can be specified with the validate
attribute. A client that sends an invalid field value in a request is likely to get an InvalidRequest
error.
Types support appropriate parameters:
length
(the valid string lengths), regex
(the pattern the string must match)value
(the valid field values)count
(the valid number of elements in an array or map)Range syntax is supported for length
, value
, and count
with ..
, e.g. 1..
(at least one), ..10
(at most ten), or 2..9
(between two and nine). Both boundaries are inclusive.
data Person
{
[validate(count: 1..)] // at least one
emailAddress: string[];
[validate(value: 0..120)] // at least zero, at most 120
age: int32;
address: Address;
}
data Address
{
[validate(regex: "^[0-9]")] // start with a digit
streetAddress: string;
[validate(length: 2)] // exactly 2
countryCode: string;
}
data PhoneNumber
{
[validate] // only 'mobile', 'work', and 'home' are valid
line: Line,
[validate(regex: "^\\+[0-9]*$", length: 3..16)] // match regex and length
number: string;
}
enum Line
{
mobile,
work,
home
}
On a request or response field, the from
parameter of the http
attribute indicates where the field comes from. It can be set to path
, query
, body
, header
, or normal
. Like all http
parameters, the from
parameter is optional; see below for defaults.
The http
attribute should not be used on a DTO field.
If from: path
is used on a request field, the field comes from the HTTP path of the method, which must contain the field name in braces. (Response fields cannot be path fields.)
If the name of a request field without a from
parameter is found in the path, it is assumed to be a path field, so from: path
never needs to be used explicitly.
For example, a getWidget
method might have a /widgets/{id}
path and a corresponding id
request field.
[http(method: GET, path: "/widgets/{id}")]
method getWidget
{
id: string;
}:
{
…
}
If from: query
is used on a request field, that field value comes from the query string of the HTTP request. (Response fields cannot be query fields.)
The name
parameter of the http
attribute can be used to indicate the name of the field as found in the query string; if omitted, it defaults to the field name.
If a request field of a GET
or DELETE
method has no from
value, it is assumed to be a query field. For other methods like POST
, from: query
is always needed to identify query fields.
The following example uses two query fields, e.g. GET https://api.example.com/v1/widgets?q=blue&limit=10
.
[http(method: GET, path: "/widgets")]
method getWidgets
{
[http(name: "q")] query: string;
limit: int32;
}:
{
…
}
If from: body
is used on a request or response field, the field value comprises the entire request or response body. The name of the field is not used in the actual HTTP request or response.
Only one request field may be marked as a body field, and the request must have no normal fields.
For response fields, the code
parameter of the http
attribute of the body field is used to indicate the HTTP status code used if the method is successful. If omitted, it defaults to 200
(OK).
In the response, multiple fields can use from: body
to indicate multiple possible response bodies. Each field must use a different code
.
The field type of a body field should generally be a DTO. A response body field can use boolean
to indicate an empty response; when used, the field is set to true
. The default code
for a boolean
body field is 204
(No Content).
[http(path: "/widgets")]
method createWidget
{
[http(from: body)]
widget: Widget;
}:
{
[http(from: body, code: 201)]
widget: Widget;
[http(from: body, code: 202)]
job: JobInfo;
}
If from: header
is used on a request or response field, the field is transmitted via HTTP header.
The name
parameter of the http
attribute can be used to indicate the name of the HTTP header; if omitted, it defaults to the field name.
The header value is not transformed in any way and must conform to the HTTP requirements for that header.
Headers commonly used by all service methods (Authorization
, User Agent
, etc.) are generally outside the scope of the FSD and not explicitly mentioned in each method request and/or response.
[http(method: GET, path: "/widgets/{id}")]
method getWidget
{
id: string;
[http(from: header, name: If-None-Match)]
ifNotETag: string;
}:
{
[http(from: header)]
eTag: string;
…
}
If from: normal
is used on a request or response field, the field is a normal part of the request or response body.
Except as indicated above, request and response fields are assumed to be normal fields, so from: normal
never needs to be used explicitly.
A method response may have both normal fields and body fields, in which case the set of normal fields is used by a different status code than any of the body fields.
Methods using GET
and DELETE
do not support normal fields.
[http(method: POST, path: "/widgets/search")]
method searchWidgets
{
query: Query;
limit: int32;
offset: int32;
}:
{
items: Widget[];
more: boolean;
}
Fields may be required via the [required]
attribute, or by adding an exclamation point after the field type name. This attribute results in invalid requests if decorated fields are unspecified in request parameters.
[http(method: POST, path: "/widgets")]
method createWidget
{
name: string!; // ! is shorthand for [required]
}:
{
id: int32;
}
Omitting a name will result in an invalid request error.
Data transfer objects (DTOs) are used to combine simpler data types into a more complex data type.
Each data transfer object has a name and a collection of fields. A DTO can also have attributes, a summary, and remarks.
The data
keyword starts the definition of a DTO. It is followed by the name of the DTO and optionally preceded by data attributes. The DTO fields follow the DTO name and are enclosed in braces.
data Widget
{
id: string;
name: string;
}
An enumerated type is a type of string that is restricted to a set of named values.
An enumerated type has a name and a collection of values, each of which has its own name. An enumerated type can also have attributes, a summary, and remarks.
The string stored by an enumerated type field should match the name of one of the values.
The value names of an enumerated type must be case-insensitively unique and may be matched case-insensitively but should nevertheless always be transmitted with the correct case.
The enum
keyword starts the definition of an enumerated type. It is optionally preceded by attributes.
The enumerated values are comma-delimited alphanumeric names surrounded by braces. A final trailing comma is permitted.
enum MyEnum
{
first,
second,
}
Enumerated values are always transmitted as strings, not integers.
External types enable the use of DTOs and enumerations defined in another service. This allows a data type to be defined once and then shared across multiple services.
External types do not require access to the definition of the target type. Code generators simply assume the data type will exist when the generated code is compiled or executed. It is the responsibility of the host project implementing the service to ensure any required references are resolved (for example, a C# project may add a reference to a NuGet package or another C# project containing the target data types).
The extern
keyword starts the definition of an external type. It is followed by either data
or enum
, depending on the external type being referenced. This is followed by the name of the external type.
Attributes on the external type instruct code generators how to reference the data type in generated code.
[csharp(namespace: Some.Project.Api.v1.Client)]
[js(module: "@example/some-project-api")]
extern data ExternalWidget;
As mentioned above, a failed service method call returns a service error instead of a response. A service error can also be stored in an error
field, or in a failed result<T>
field.
Each instance of a service error has a code and a message. It may also have details and an inner error.
The code is a machine-readable string
that identifies the error. There are a number of standard error codes that should be used if possible:
InvalidRequest
: The request was invalid.InternalError
: The service experienced an unexpected internal error.InvalidResponse
: The service returned an unexpected response.ServiceUnavailable
: The service is unavailable.Timeout
: The service timed out.NotAuthenticated
: The client must be authenticated.NotAuthorized
: The authenticated client does not have the required authorization.NotFound
: The specified item was not found.NotModified
: The specified item was not modified.Conflict
: A conflict occurred.TooManyRequests
: The client has made too many requests.RequestTooLarge
: The request is too large.The message is a human-readable string
that describes the error. It is usually intended for developers, not end users.
The details object
can be used to store whatever additional error information the service wants to communicate.
The inner error is an error
that can be used to provide more information about what caused the error, especially if it was caused by a dependent service that failed.
A service result can be used in response fields by methods that perform multiple operations and want to return separate success or failure for each one.
An instance of a service result contains exactly one of the following:
A service that needs to support non-standard error codes can define its own error set, which supplements the standard error codes.
Each error set has a name and a collection of values, each of which has its own name. An error set can also have attributes, a summary, and remarks.
The name of each error set value represents a supported error code.
The documentation summary of each error set value is used as the default error message for that error code.
The errors
keyword starts an error set. It is followed by the name of the error set and optionally preceded by attributes.
The error values are comma-delimited alphanumeric names surrounded by braces. A final trailing comma is permitted.
On an error of an error set, the code
parameter of the http
attribute is used to specify the HTTP status code that should be used when that error is returned, e.g. 404
.
If the code
parameter is missing from an error, 500
(Internal Server Error) is used.
errors MyErrors
{
[http(code: 503)]
OutToLunch
}
The standard error codes already have reasonable HTTP status codes:
InvalidRequest
: 400
InternalError
: 500
InvalidResponse
: 500
ServiceUnavailable
: 503
Timeout
: 500
NotAuthenticated
: 401
NotAuthorized
: 403
NotFound
: 404
NotModified
: 304
Conflict
: 409
TooManyRequests
: 429
RequestTooLarge
: 413
Use two slashes to start a comment. The slashes and everything that follows them on that line are ignored (except for summaries).
data MyData // this comment is ignored
{
name: string; // so is this
// and this
}
Most elements of a service support a summary string for documentation purposes: service, methods, DTOs, fields, enumerated types, enumerated type values, error sets, and error set values.
The summary should be short and consist of a single sentence or short paragraph.
To add a summary to a service element, place a comment line before it that uses three slashes instead of two. Multiple summary comments can be used for a single element of a service; newlines are automatically replaced with spaces.
Summaries are supported by services, methods, DTOs, fields, enumerated types, enumerated values, error sets, and error values.
/// My awesome data.
data MyData
{
…
}
Some elements also support remarks: service, methods, DTOs, enumerated types, and error sets.
The remarks can use GitHub Flavored Markdown. They can be arbitrarily long and can include multiple paragraphs.
Add remarks to an FSD file after the end of the closing brace of the service
.
The first non-blank line immediately following the closing bracket must be a top-level #
heading (e.g. # myMethod
).
That first heading as well as any additional top-level headings must match the name of the service or a method, DTO, enumerated type, or error set. Any text under that heading represents additional documentation for that part of the service.
/// The service summary.
service MyApi
{
/// The method summary.
method myMethod { … }: { … }
/// The DTO summary.
data MyData { … }
}
# MyApi
These are the remarks for the entire service.
# myMethod
Here are the remarks for one of the service methods.
# MyData
Here are the remarks for one of the service DTOs.
fsdgenfsd
is a rarely-used command-line tool that generates “canonical” FSD for a Facility Service Definition.
Install fsdgenfsd
as documented from its NuGet package.
fsdgenfsd
generates an FSD file and supports the standard command-line options.