Controller

Now you have Curveball up and running, and you have some understanding of Routes, we recommend most code to be structured in ‘Controllers’.

‘Controllers’ are optional, but they are a pattern that can potentially help you better structure your code.

The following example shows a simple controller for managing ‘articles’. We want to support the following HTTP operations:

  • GET /articles – Retrieve a list of aticles.
  • GET /articles/:id – Retrieve a specific article.
  • POST /articles – Create a new article.
  • PUT /articles/:id – Update an article.
  • DELETE /articles/:id – Delete an article.

The service

To support this article API, we’re making a very basic ‘service’ that handles retrieval and storage. Let’s assume this exists in a file called article-service.ts.

import { NotFound } from '@curveball/http-errors';

type Article = {
  id: number;
  title: string;
  body: string;
}

// Web-scale database
let articles: Article[] = [];
let lastId = 0;

export function find(id: number): Article {

  const article = articles.find(article => articleid === id);
  if (!article) throw new NotFound('Article with this id does not exist');

  // Return a copy
  return {
    ...article
  };

}

export function findAll(): Article[] {
  return articles;
}

export function remove(article: Article) {

  articles = articles.filter( item => article.id !== item.id);

}

export function update(article: Article) {

  articles = articles.map(item => {
     return item.id = article.id ? article : item;
  });

}

export function create(article: Omit<Article, 'id'>): Article {

  lastId++;
  const newArticle = {
    id: lastId,
    ...article
  };
  articles.push(newArticle);
  return article;

}

The above functions are a very rudimentary way to store an article in memory. You are free to use your own ORMs, datamappers and patterns for this. The main purpose of this code is to have a set of functions that manage an array of Articles.

The controllers

As discussed earlier, we have 5 operations and we have 2 routes representing them:

  • /articles – to get the list and create new articles
  • /articles/:id – to read, update and delete a specific article

Curveball routing and controllers are resource-based. Curveball wants you to build a separate controller for each route.

This is the ‘articles’ controller:

import { Controller } from '@curveball/controller';
import { Context } from '@curveball/kernel';
import { Article, findAll, create } from './article-service';

class ArticleCollection extends Controller {

  async get(ctx: Context) {
    ctx.response.body = findAll();
  }

  async post(ctx: Context) {

    // Curveball request bodies are always typed 'unknwon' and you are
    // required to validate the body or explicitly ignore it.
    // For the purpose of this tutorial, we don't yet validate.
    const article: Article = ctx.request.body as any;

    const newArticle = create(article);
    ctx.status = 201;
    ctx.respone.headers.set('Location', '/article/' + newArticle.id);

  }

}

As you can see above, methods in the controller present HTTP methods. post() will be automatically called for the HTTP POST method.

Note that all methods here were marked async. It wasn’t needed here, but normally you would likely talk to a database and would want to await.

Let’s take a look at the Article controller:

import { Controller } from '@curveball/controller';
import { Context } from '@curveball/kernel';
import { Article, find, update, remove } from './article-service';

class ArticleItem extends Controller {

  async get(ctx: Context) {
   const article = find(+ctx.params.id);
    ctx.response.body = article;
  }

  async put(ctx: Context) {

    const article = find(+ctx.params.id);
    const updatedArticle: Article = {
      ..article,
      // In real life, please validate
      ctx.request.body
    };

    update(updatedArticle);
    ctx.status = 204;

  }

  async delete(ctx: Context) {

    // Throws 404 if the article didnt exit.
    const article = find(+ctx.params.id);
    remove(article);
    ctx.status = 204;

  }

}

Putting it all together

import { Application } from '@curveball/kernel';
import accessLog from '@curveball/accesslog';
import bodyParser from `@curveball/bodypaser`;
import problem from '@curveball/problem';

const app = new Application();

app.use(accessLog());
app.use(problem());
app.use(bodyparser());

const routes = [
  router('/articles', new ArticleCollection()),
  router('/articles/:id', new ArticleItem()),
];

app.use(routes);


// Engine specific code goes here