annuaire tech France

Tinyweb Series, Part 2: Building APIs

In part 1 we built a front handler, a dispatcher and controllers that return result objects. Now we make Tinyweb useful to other programs by adding API support: JSON, XML, status codes and HTTP verbs.

Results do the formatting

The design from part 1 pays off here. An action returns a TinyResult, and the result decides how to turn data into bytes. The action only knows about data:

public class ProductsController
{
    public TinyResult Get(int id)
    {
        var product = Catalog.Find(id);
        if (product == null) return new StatusResult(404, "No such product");
        return new DataResult(product);
    }
}

Whether DataResult becomes JSON or XML is decided later. That keeps controllers free of formatting code and trivial to test, because you assert on the object inside the result.

A JSON result

.NET offers serializers in the framework, so there is no need to write one. A JSON result can use JavaScriptSerializer from System.Web.Script.Serialization or a data contract serializer, depending on your framework version and taste.

public class JsonResult : TinyResult
{
    private readonly object _data;
    public JsonResult(object data) { _data = data; }

    public override void Execute(HttpContext context)
    {
        context.Response.ContentType = "application/json";
        context.Response.Write(new JavaScriptSerializer().Serialize(_data));
    }
}

An XML result

For XML, XmlSerializer handles plain classes with public properties:

public class XmlResult : TinyResult
{
    private readonly object _data;
    public XmlResult(object data) { _data = data; }

    public override void Execute(HttpContext context)
    {
        context.Response.ContentType = "application/xml";
        var serializer = new XmlSerializer(_data.GetType());
        serializer.Serialize(context.Response.Output, _data);
    }
}

Content negotiation, the small version

Real content negotiation parses quality values from the Accept header. Tinyweb can start with something far simpler: look at a format hint, then fall back to the header.

public class DataResult : TinyResult
{
    private readonly object _data;
    public DataResult(object data) { _data = data; }

    public override void Execute(HttpContext context)
    {
        var format = context.Request.QueryString["format"];
        var accept = context.Request.Headers["Accept"] ?? "";

        TinyResult inner;
        if (format == "xml" || accept.Contains("application/xml"))
            inner = new XmlResult(_data);
        else
            inner = new JsonResult(_data);

        inner.Execute(context);
    }
}

It is crude, and it is honest. You can improve it later without touching a single controller, which is the benefit of keeping decisions in one place.

Status codes and errors

An API that always answers 200 forces clients to read the body to find out what happened. Give actions a way to say it:

public class StatusResult : TinyResult
{
    private readonly int _code;
    private readonly string _message;

    public StatusResult(int code, string message)
    {
        _code = code;
        _message = message;
    }

    public override void Execute(HttpContext context)
    {
        context.Response.StatusCode = _code;
        context.Response.ContentType = "text/plain";
        context.Response.Write(_message);
    }
}

The common ones to use are 200 for success, 201 after creating something, 400 for bad input, 404 when something does not exist and 500 when your own code failed. Keep your own error responses consistent so clients can handle them in one place.

HTTP verbs

An API does different things for different verbs on the same URL. Tinyweb can support this with a naming convention: an action named Get, Post, Put or Delete is chosen by request.Method.

private MethodInfo FindAction(Type controller, TinyRequest request, string actionName)
{
    var verbName = request.Method.Substring(0, 1) + request.Method.Substring(1).ToLowerInvariant();

    return controller.GetMethods().FirstOrDefault(m =>
        string.Equals(m.Name, actionName, StringComparison.OrdinalIgnoreCase) ||
        string.Equals(m.Name, verbName, StringComparison.OrdinalIgnoreCase));
}

With that, GET /products?id=3 invokes Get and POST /products invokes Post. The convention is simple enough to remember and easy to replace with attributes later if you prefer explicitness.

Accepting data

For POST and PUT the client sends data in the body. In this part, we accept form-encoded values through the same query-and-form binder as before. JSON request bodies are a worthwhile extension: read the input stream, deserialize into the parameter type, and pass the object to the action. We handle richer binding in part 4.

Testing the API

Because actions return result objects, tests stay small:

[TestMethod]
public void Missing_product_returns_404()
{
    var result = new ProductsController().Get(999) as StatusResult;

    Assert.IsNotNull(result);
    Assert.AreEqual(404, result.Code);
}

(Expose Code as a public read-only property for this to compile.) When an action reads the clock or the disk, the delegate trick from funcy testing keeps the test equally small, and for stubborn static code you can still fall back on isolating code with Moles and Fakes.

Checklist for a decent API

  1. Use nouns in URLs and verbs in HTTP methods.
  2. Return meaningful status codes, not 200 with an error in the body.
  3. Version your responses before you have outside users, not after.
  4. Document the shape of every response.
  5. Serialize dedicated response types, not entities.

Our controllers are now talking to programs as well as browsers, but they still create their own dependencies. Part 3 sorts that out.

More .NET guides

Looking for a software partner? Browse the independent directory of French tech companies (web agencies, IT consultancies, software publishers; the directory itself is in French).