API Pages

In addition to being productive high-level .NET scripting language for generating dynamic HTML pages, Template Pages can also be used to rapidly develop Web APIs which can take advantage of the new support for Dynamic Page Based Routes to rapidly develop data-driven JSON APIs and make them available under ideal pretty URLs whilst utilizing the same Live Development workflow that doesn't need to define any C# Types or execute any builds - as all development can happen in real-time whilst the App is running, enabling the fastest way to develop Web APIs in .NET.

Dynamic API Pages

The only difference between a Template Page that generates HTML or a Template Page that returns an API Response is that API pages return a value using the return filter.

For comparison, to create a Hello World C# ServiceStack Service you would typically create a Request DTO, Response DTO and a Service implementation:

[Route("/hello/{Name}")]
public class Hello : IReturn<HelloResponse>
{
    public string Name { get; set; }
}
public class HelloResponse
{
    public string Result { get; set; }
}
public class HelloService : Service
{
    public object Any(Hello request) => new HelloResponse { Result = $"Hello, {request.Name}!" };
}

/hello API Page

Usage: /hello/{name}

An API which returns the same wire response as above can be implemented in API Pages by creating a page at /hello/_name/index.html that includes the 1-liner:

{{ { result: `Hello, ${name}!` } | return }}

Which supports the same content negotiation as a ServiceStack Service where calling it in a browser will generate a human-friendly HTML Page:

Or calling it with a JSON HTTP client containing Accept: application/json HTTP Header or with a ?format=json query string will render the API response in the JSON Format:

Alternatively you can force a JSON Response by specifying the Content Type in the return arguments:

{{ { result: `Hello, ${name}!` } | return({ format: 'json' }) }} 
// Equivalent to:
{{ { result: `Hello, ${name}!` } | return({ contentType: 'application/json' }) }}

More API examples showing the versatility of this feature is contained in the new blog.web-app.io which only uses Templates and Dynamic API Pages to implement all of its functionality.

/preview API Page

Usage: /preview?content={templates}

The /preview.html API page uses this to force a plain-text response with:

{{ content  | evalTemplate({use:{plugins:'MarkdownTemplatePlugin'}}) | assignTo:response }}
{{ response | return({ contentType:'text/plain' }) }}

The preview API above is what provides the new Blog Web App's Live Preview feature where it will render any ServiceStack Templates provided in the content Query String or HTTP Post Form Data, e.g:

Which renders the plain text response:

0,1,4,9,16,25,36,49,64,81,

/_user/api Page

Usage: /{user}/api

The /_user/api.html API page shows an example of how easy it is to create data-driven APIs where you can literally return the response of a parameterized SQL query using the dbSelect filter and returning the results:

{{ `SELECT * 
      FROM Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName 
     WHERE UserName = @user 
    ORDER BY p.Created DESC` 
   | dbSelect({ user })
   | return }}

The user argument is populated as a result of dynamic route from the _user directory name which will let you view all @ServiceStack posts with:

Which also benefits from ServiceStack's multiple built-in formats where the same API can be returned in:

/posts/_slug/api Page

Usage: /posts/{slug}/api

The /posts/_slug/api.html page shows an example of using the httpResult filter to return a custom HTTP Response where if the post with the specified slug does not exist it will return a 404 Post was not found HTTP Response:

{{ `SELECT * 
      FROM Post p INNER JOIN UserInfo u on p.CreatedBy = u.UserName 
     WHERE Slug = @slug 
     ORDER BY p.Created DESC` 
   | dbSingle({ slug })
   | assignTo: post 
}}
{{ post ?? httpResult({ status:404, statusDescription:'Post was not found' }) 
   | return }}

The httpResult filter returns a ServiceStack HttpResult which allows for the following customizations:

httpResult({ 
  status:            404,
  status:            'NotFound' // can also use .NET HttpStatusCode enum name
  statusDescription: 'Post was not found',
  response:          post,
  format:            'json',
  contentType:       'application/json',
  'X-Powered-By':    'ServiceStack Templates',
}) 

Any other unknown arguments like 'X-Powered-By' are returned as HTTP Response Headers.

Returning the httpResult above behaves similarly to customizing a HTTP response using return arguments:

{{ post | return({ format:'json', 'X-Powered-By':'ServiceStack Templates' }) }}

Using the explicit httpResult filter is useful for returning a custom HTTP Response without a Response Body, e.g. the New Post page uses httpFilter to redirect back to the Users posts page after they've successfully created a new Post:

{{#if success}}
    {{ httpResult({ status:301, Location:`/${userName}` }) | return }}
{{/if}}

Dedicated API Pages

Dedicated API pages lets you specify a path where your "API Pages" are located when registering TemplatePagesFeature:

Plugins.Add(new TemplatePagesFeature 
{
    ApiPath = "/api"
})

All pages within the /api folder are also treated like "API Pages" for creating Web APIs where instead of writing their response to the Output Stream, their return value is serialized in the requested Content-Type using the return filter:

{{ response | return }}
{{ response | return({ ... }) }}
{{ httpResult({ ... }) | return }}

The route for the dedicated API page starts the same as the filename and one advantage over Dynamic API Pages above is that a single Page can handle multiple requests with different routes, e.g:

/api/customers                // PathArgs = []
/api/customers/1              // PathArgs = ['1']
/api/customers/by-name/Name   // PathArgs = ['by-name','Name']

API Page Examples

To demonstrate API Pages in action we've added Web APIs equivalents for Rockwind's customers and products HTML pages with the implementation below:

/api/customers

The entire implementation of the customers API is below:

{{ limit ?? 100 | assignTo: limit }}

{{ 'select Id, CompanyName, ContactName, ContactTitle, City, Country from Customer' | assignTo: sql }}

{{#if !isEmpty(PathArgs)}}
   {{ `${sql} where Id = @id` | dbSingle({ id: PathArgs[0] }) 
      | return }}
{{/if}}

{{#if id}}      {{ 'Id = @id'           | addTo: filters }} {{/if}}
{{#if city}}    {{ 'City = @city'       | addTo: filters }} {{/if}}
{{#if country}} {{ 'Country = @country' | addTo: filters }} {{/if}}

{{#if !isEmpty(filters)}}
  {{ `${sql} WHERE ${join(filters, ' AND ')}` | assignTo: sql }}
{{/if}}

{{ `${sql} ORDER BY CompanyName ${sqlLimit(limit)}` | assignTo: sql }}

{{ sql | dbSelect({ id, city, country }) 
       | return }}

These are some of the API's that are made available with the above implementation:

/customers API
All Customers
Accept HTTP Header also supported
Alfreds Futterkiste Details
As List
Customers in Germany
Customers in London
Combination Query /api/customers?city=London&country=UK&limit=3

/api/products

The Products API is an example of a more complex API where data is sourced from multiple tables:

{{ limit ?? 100 | assignTo: limit }}

{{ `select p.Id, 
           ProductName,
           c.CategoryName Category,
           s.CompanyName Supplier, 
           QuantityPerUnit, 
           ${sqlCurrency("UnitPrice")} UnitPrice, 
           UnitsInStock, UnitsOnOrder, ReorderLevel   
      from Product p
           inner join Category c on p.CategoryId = c.Id
           inner join Supplier s on p.SupplierId = s.Id
     where Discontinued = 0`
  | assignTo: sql }}

{{#if !isEmpty(PathArgs)}}
  {{ `${sql} and p.Id = @id` | dbSingle({ id: PathArgs[0] }) 
     | return }}
{{/if}}

{{#if id}}           {{ 'p.Id = @id'                 | addTo: filters }} {{/if}}
{{#if category}}     {{ 'c.CategoryName = @category' | addTo: filters }} {{/if}}
{{#if supplier}}     {{ 's.CompanyName = @supplier'  | addTo: filters }} {{/if}}
{{#if nameContains}} {{ 'ProductName LIKE @name'     | addTo: filters }} {{/if}}

{{#if !isEmpty(filters)}}
  {{ `${sql} and ${join(filters, ' and ')}` | assignTo: sql }}
{{/if}}

{{ `${sql} ORDER BY CompanyName ${sqlLimit(limit)}` | assignTo: sql }}

{{ sql | dbSelect({ id, category, supplier, name: `%${nameContains}%` }) 
       | return }}

Some API examples using the above implementation:

/products API
All Products
Chai Product Details
As List
Beverage Products
Bigfoot Breweries Products
Products containing Tofu

Untyped APIs

As these APIs don't have a Typed Schema they don't benefit from any of ServiceStack's metadata Services, i.e. they're not listed in Metadata pages, included in Open API or have Typed APIs generated using Add ServiceStack Reference.

made with by ServiceStack