Routes and model binding (Intermediate)

5 min read

(For more resources related to this topic, see here.)

Getting ready

This section builds on the previous section and assumes you have the TodoNancy and TodoNancyTests projects all set up.

How to do it…

The following steps will help you to handle the other HTTP verbs and work with dynamic routes:

  1. Open the TodoNancy Visual Studio solution.
  2. Add a new class to the NancyTodoTests project, call it TodosModulesTests, and fill this test code for a GET and a POST route into it:

    public class TodosModuleTests { private Browser sut; private Todo aTodo; private Todo anEditedTodo; public TodosModuleTests() {; sut = new Browser(new DefaultNancyBootstrapper()); aTodo = new Todo { title = "task 1", order = 0, completed = false }; anEditedTodo = new Todo() { id = 42, title = "edited name", order = 0, completed = false }; } [Fact] public void Should_return_empty_list_on_get_when_no_todos_have_been
    _posted() { var actual = sut.Get("/todos/"); Assert.Equal(HttpStatusCode.OK, actual.StatusCode); Assert.Empty(actual.Body.DeserializeJson<Todo[]>()); } [Fact] public void Should_return_201_create_when_a_todo_is_posted() { var actual = sut.Post("/todos/", with => with.JsonBody(aTodo)); Assert.Equal(HttpStatusCode.Created, actual.StatusCode); } [Fact] public void Should_not_accept_posting_to_with_duplicate_id() { var actual = sut.Post("/todos/", with => with.JsonBody(anEditedTodo)) .Then .Post("/todos/", with => with.JsonBody(anEditedTodo)); Assert.Equal(HttpStatusCode.NotAcceptable, actual.StatusCode); } [Fact] public void Should_be_able_to_get_posted_todo() { var actual = sut.Post("/todos/", with => with.JsonBody(aTodo) ) .Then .Get("/todos/"); var actualBody = actual.Body.DeserializeJson<Todo[]>(); Assert.Equal(1, actualBody.Length); AssertAreSame(aTodo, actualBody[0]); } private void AssertAreSame(Todo expected, Todo actual) { Assert.Equal(expected.title, actual.title); Assert.Equal(expected.order, actual.order); Assert.Equal(expected.completed, actual.completed); } }

  3. The main thing to notice new in these tests is the use of actual.Body.DesrializeJson<Todo[]>(), which takes the Body property of the BrowserResponse type, assumes it contains JSON formatted text, and then deserializes that string into an array of Todo objects.
  4. At the moment, these tests will not compile. To fix this, add this Todo class to the TodoNancy project as follows:

    public class Todo { public long id { get; set; } public string title { get; set; } public int order { get; set; } public bool completed { get; set; } }

  5. Then, go to the TodoNancy project, and add a new C# file, call it TodosModule, and add the following code to body of the new class:

    public static Dictionary<long, Todo> store = new Dictionary<long, Todo>();

  6. Run the tests and watch them fail. Then add the following code to TodosModule:

    public TodosModule() : base("todos") { Get["/"] = _ => Response.AsJson(store.Values); Post["/"] = _ => { var newTodo = this.Bind<Todo>(); if ( == 0) = store.Count + 1; if (store.ContainsKey( return HttpStatusCode.NotAcceptable; store.Add(, newTodo); return Response.AsJson(newTodo) .WithStatusCode(HttpStatusCode.Created); }; }

  7. The previous code adds two new handlers to our application. One handler for the GET “/todos/” HTTP and the other handler for the POST “/todos/” HTTP. The GET handler returns a list of todo items as a JSON array. The POST handler allows for creating new todos. Re-run the tests and watch them succeed.
  8. Now let’s take a closer look at the code. Firstly, note how adding a handler for the POST HTTP is similar to adding handlers for the GET HTTP. This consistency extends to the other HTTP verbs too. Secondly, note that we pass the “todos”string to the base constructor. This tells Nancy that all routes in this module are related to /todos. Thirdly, notice the this.Bind<Todo>() call, which is Nancy’s data binding in action; it deserializes the body of the POST HTTP into a Todo object.
  9. Now go back to the TodosModuleTests class and add these tests for the PUT and DELETE HTTP as follows:

    [Fact] public void Should_be_able_to_edit_todo_with_put() { var actual = sut.Post("/todos/", with => with.JsonBody(aTodo)) .Then .Put("/todos/1", with => with.JsonBody(anEditedTodo)) .Then .Get("/todos/"); var actualBody = actual.Body.DeserializeJson<Todo[]>(); Assert.Equal(1, actualBody.Length); AssertAreSame(anEditedTodo, actualBody[0]); } [Fact] public void Should_be_able_to_delete_todo_with_delete() { var actual = sut.Post("/todos/", with => with.Body(aTodo.ToJSON())) .Then .Delete("/todos/1") .Then .Get("/todos/"); Assert.Equal(HttpStatusCode.OK, actual.StatusCode); Assert.Empty(actual.Body.DeserializeJson<Todo[]>()); }

  10. After watching these tests fail, make them pass by adding this code to the constructor of TodosModule:

    Put["/{id}"] = p => { if (!store.ContainsKey( return HttpStatusCode.NotFound; var updatedTodo = this.Bind<Todo>(); store[] = updatedTodo; return Response.AsJson(updatedTodo); }; Delete["/{id}"] = p => { if (!store.ContainsKey( return HttpStatusCode.NotFound; store.Remove(; return HttpStatusCode.OK; };

  11. All tests should now pass.
  12. Take a look at the routes to the new handlers for the PUT and DELETE HTTP. Both are defined as “/{id}”. This will match any route that starts with /todos/ and then something more that appears after the trailing /, such as /todos/42 and the {id} part of the route definition is 42. Notice that both these new handlers use their p argument to get the ID from the route in the expression. Nancy lets you define very flexible routes. You can use any regular expression to define a route. All named parts of such regular expressions are put into the argument for the handler. The type of this argument is DynamicDictionary, which is a special Nancy type that lets you look up parts via either indexers (for example, p[“id”]) like a dictionary, or dot notation (for example, like other dynamic C# objects.

There’s more…

In addition to the handlers for GET, POST, PUT, and DELETE, which we added in this recipe, we can go ahead and add handler for PATCH and OPTIONS by following the exact same pattern.

Out of the box, Nancy automatically supports HEAD and OPTIONS for you. To handle the HEAD HTTP request, Nancy will run the corresponding GET handler but only return the headers. To handle OPTIONS, Nancy will inspect which routes you have defined and respond accordingly.


In this article we saw how to handle the other HTTP verbs apart from GET and how to work with dynamic routes. We will also saw how to work with JSON data and how to do model binding.

Resources for Article:

Further resources on this subject:


Please enter your comment!
Please enter your name here