Page Contents

Define the API

Start with data

When building an API, its usually easiest to start by outlining some example data that consumers of the API will need. This can act as the first rough draft of the API specification for smaller applications / APIs. In this tutorial, you’ll start by sketching out some example API response data as simple JavaScript objects:

const products = [
  {name: 'Headphones', price: 29.99, category: '/categories/accessories', available: true, deals: ['50% off', 'free shipping']},
  {name: 'Mouse', price: 32.99, category: '/categories/accessories', available: true, deals: ['30% off', 'free shipping']},
  {name: 'yPhone', price: 299.99, category: '/categories/phones', available: true, deals: ['free shipping']},
  {name: 'yBook', price: 5999.99, category: '/categories/computers', available: true}
];

With the example data defined, you can start to get an idea of how to separate the data into individual proper nouns, which will eventually be defined in different ways. Either as resources, schemas, models, or repositories.

  • CatalogItem - Each object in the array above
  • Category - Has a URL, and more information about the category
  • Product - The name, price and other product information
  • Inventory - The product availability
  • Deals - Information about promotions on a group of products

Outline the API

With the proper nouns of the API defined, you can now start to think about what the API will look like.

This is where you choose how fine or coarse grain the API will be. You have to decide which proper nouns above will be available as Resources. The easiest way to figure out which Resources are needed is by sketching out the URLs (without verbs) for the API:

  • /products?{query} - Search for products in the catalog
  • /product/{slug} - Get the details for a particular product
  • /deals?{query} - Search for deals
  • /deal/{slug} - Get the details for a particular deal
  • /categories?{query} - Search for categories
  • /category/{slug} - Get the details for a particular category
  • /category/{slug}/products?{query} - Search for products in a particular category

Break down the data into resources

With the URLs, defined, its easy to determine which resources you’ll need.

  • ProductResource
  • DealResource
  • CategoryResource

This is where it’s useful to determine similarities between Resources; for example, the ProductResource, DealResource, and CategoryResource all have the same URL structure, with the exception of /category/{slug}/products?{query} path on CategoryResource:

  • /{pluralName}?{query} - Search with a query and the resource plural name
  • /{name}/{slug} - Get details about the resource

Using patterns to reduce duplication

It can be tricky to determine the patterns on which to base the API, since you’ll likely want to change it in the future. To keep the patterns flexible, you can define these patterns via simple TypeScript functions (you can also do it in JavaScript). Start with a SearchableResource pattern, since all of the resources must support the same search and listing operations.

The SearchableResource pattern will define all of the semantics for an OpenAPI fragment that supports search.

/apidefs/templates/searchable-resource.ts

export let searchableResource = (resource: any, type: string) => ({
  paths: {
    [`/${resource.path}`]: { // pattern
      get: {
        "parameters": [{
          in: "query",
          name: "filter",
          type: "string",
        }],
        "responses": {
          200: {
            description: resource.description ||
              `Result set of type ${type} returned.`,
            schema: {
              $ref: `#/definitions/${type}`,
              type: "array",
            },
          },
        },
        "x-controller-name": resource.controller,
        "x-operation-name": "search",
      },
    },
    [`/${resource.path}/{slug}`]: { // pattern
      get: {
        "parameters": [
          {
            in: "path",
            name: "slug",
            required: true,
            type: "string",
          },
        ],
        "responses": {
          200: {
            description: resource.description ||
              `Result of type ${type} returned.`,
            schema: {
              $ref: `#/definitions/${type}`,
            },
          },
        },
        "x-controller-name": resource.controller,
        "x-operation-name": "getDetails",
      },
    },
  },
});

Here’s another example for creating a POST template, called CreatableResource.

/apidefs/templates/creatable-resource.ts

export let creatableResource = (resource: any, type: string) => ({
  paths: {
    [`/${resource.path}`]: { // pattern
      post: {
        "parameters": [
          {
            in: "body",
            name: "body",
            required: true,
            schema: {
                $ref: `#/definitions/${type}`,
            },
          },
        ],
        "responses": {
          201: {
            description: resource.description
              || `The ${type} instance was created.`,
            schema: {
                $ref: `#/definitions/${type}`,
            },
          },
        },
        "x-controller-name": resource.controller,
        "x-operation-name": "create",
      },
    },
  },
});

Lastly, you’ll create a helper function for generating type definitions in OpenAPI.

/apidefs/templates/type-definition.ts

import { DefinitionsObject } from "@loopback/openapi-spec";

export let TypeDefinition = (type: any): DefinitionsObject => ({
  definitions: {
    [`${type.name}`]: {
        properties: type.definition,
    },
  },
});

Given the pattern function above, you can now create the OpenAPI fragment that represents the ProductController portion of the full specification. This example, uses lodash to help with merging generated definitions together. Install lodash with this command:

npm install --save lodash

/apidefs/product.api.ts

import * as _ from "lodash";

// Assuming you have created the "base" schema elsewhere.
// If there are no common properties between all of the endpoint objects,
// then you can ignore this.
import BaseSchema from "../BaseSchema";
// Don't forget to export the template functions under a common file!
import { SearchableResource, CreatableResource, TypeDefinition } from "./templates";
let ProductAPI: ControllerSpec = {};

const ProductDefinition = {};
// Build type definition using base schema + additional properties.
_.merge(ProductDefinition, BaseSchema, TypeDefinition({
  price: {
    type: "number",
    minimum: 0,
    exclusiveMinimum: true,
  }
}));

const ProductGetResource = SearchableResource({
  controller: "ProductController",
  operation: "search",
  path: "products",
}, "Product");

const ProductCreateResource = CreatableResource({
  controller: "ProductController",
  operation: "create",
  path: "products",
}, "Product");
// Rinse and repeat for PUT, PATCH, DELETE, etc...

// Merge all of the objects together.
// This will mix the product definition into the "definitions" property of the
// OpenAPI spec, and the resources will be mixed into the "paths" property.
_.merge(ProductAPI, ProductDefinition, ProductGetResource,
  ProductCreateResource);

// And export it!
export default ProductAPI;

Connect OpenAPI fragments to Controllers

By separating each individual “Model”-level API export, you can link them to their corresponding controllers throughout the application.

/controllers/product-controller.ts

import { api } from "@loopback/core";
import ProductApi from "../apidefs/product.api";

// This decorator binds the Product API to the controller,
// which will establish routing to the specified functions below.
@api(ProductApi)
export class ProductController {

  // Note that the function names here match the strings in the "operation"
  // property you provided to the SearchableResource call in the previous
  // example.
  public search() {
    // your logic here
  }

  // Same goes for this function!
  public create(id: number, name: string, price: number) {
    // your logic here
  }

  // etc...
}

Putting together the final API specification

Now that you’ve built the OpenAPI fragments for each of the controllers, you can put them all together to produce the final OpenAPI spec.

/apidefs/swagger.ts

import { ProductAPI, DealAPI, CategoryAPI } from "../apidefs";
import * as OpenApiSpec from "@loopback/openapi-spec";
import * as _ from "lodash";


// Import API fragments here

export let spec = OpenApiSpec.createEmptyApiSpec();
spec.info = {
  title: "Your API",
  version: "1.0",
};
spec.swagger = "2.0";
spec.basePath = "/";

_.merge(spec, ProductAPI);
_.merge(spec, DealAPI);
_.merge(spec, CategoryAPI);

export default spec;

You can then bind the full spec to the application using server.spec(). This is done on the server level, because each server instance can expose a different (sub)set of API.

You also need to associate the controllers implementing the spec with the app using app.controller(GreetController). This is not done on the server level because a controller may be used with multiple server instances, and types!

// application.ts
// This should be the export from the previous example.
import spec from "../apidefs/swagger";
import { Application } from "@loopback/core";
import { RestComponent, RestServer } from "@loopback/rest";
import { ProductController, DealController, CategoryController } from "./controllers";
export class YourMicroservice extends Application {

  constructor() {
    super({
      components: [RestComponent],
    });
    const app = this;

    app.controller(ProductController);
    app.controller(DealController);
    app.controller(CategoryController);

  }
  async start() {
    const server = await app.getServer(RestServer);
    // inject your spec here!
    server.api(spec);
    server.bind("rest.port").to(3001);
    await super.start();
  }
  // etc...
}

Validate the API specification

The OpenAPI Swagger editor is a handy tool for editing OpenAPI specifications that comes with a built-in validator. It can be useful to manually validate an OpenAPI specification.

However, manual validation is tedious and error prone. It’s better to use an automated solution that’s run as part of a CI/CD workflow. LoopBack’s testlab module provides a helper function for checking whether a specification conforms to OpenAPI Spec. Just add a new Mocha test that calls this helper function to the test suite:

// test/acceptance/api-spec.acceptance.ts

import {validateApiSpec} from '@loopback/testlab';
import {MyApp} from '../..';
import {RestServer} from '@loopback/rest';

describe('API specification', () => {
  it('api spec is valid', async () => {
    const app = new MyApp();
    const server = await app.getServer(RestServer);
    const spec = server.getApiSpec();
    await validateApiSpec(apiSpec);
  });
});

See Validate your OpenAPI specification from Testing your application for more details.

Next: Testing the API