Divotion Blog

Publish your backend API typings as an NPM package

Published on by Christophe

In this post I suggest a way to publish your backend API typings as an NPM package. Frontend projects using can then depend on these typings to gain compile time type safety and code completion when using Typescript.

It’s quite common in a microservice style architecture to provide a type-safe client library that other services can use to communicate with your service. This can be package with a Retrofit client published to nexus by the maintainer of the service. Some projects might also generate that code from a OpenAPI spec or a gRPC proto file.

However, when we expose some of these APIs to the frontend, we lose the types. In this post I suggest a way to publish your backend API types as an NPM package. The frontend can then depend on these typings. Using typescript you now have compile time type safety and code completion. To see a sneak peak, scroll to the end :).

The next steps provide 'a way' to publish typings of a simple Kotlin service (also works for java) to the NPM registry. Then we will use a simple frontend to depend on these types.

A complete working example is available at github.com/toefel18/backend-api-as-npm-typings-blog

Creating a simple service

The following code sample uses Javalin to create a simple REST API that publishes a movie:

enum class Genre {
    ACTION,
    COMEDY,
    THRILLER,
    HORROR,
    DRAMA
}

data class ActorDto(
    val firstName: String,
    val lastName: String,
    val dateOfBirth: LocalDate?
)

data class MovieDto(
    val name: String,
    val producerName: String,
    val releaseDate: LocalDateTime,
    val genre: Genre,
    val actors: List<ActorDto>
)

class Router(val port: Int) {
    private val logger: Logger = LoggerFactory.getLogger(Router::class.java)

    val app = Javalin.create { cfg -> cfg.requestLogger(::logRequest).enableCorsForAllOrigins() }
        .get("/movies", ::listMovies)

    private fun logRequest(ctx: Context, executionTimeMs: Float) =
        logger.info("${ctx.method()} ${ctx.fullUrl()} status=${ctx.status()} durationMs=$executionTimeMs")

    fun start(): Router {
        app.start(port)
        return this
    }

    fun listMovies(ctx: Context) {
        val bradPitt = ActorDto(
            firstName = "Johnny",
            lastName = "Depp",
            dateOfBirth = LocalDate.parse("1989-07-17")
        )
        val johnnyDepp = ActorDto(
            firstName = "Brad",
            lastName = "Pitt",
            dateOfBirth = LocalDate.parse("1990-11-03")
        )

        val lionKing = MovieDto(
            name = "Lion King",
            producerName = "Disney",
            releaseDate = LocalDateTime.parse("2019-07-17T15:30:00"),
            genre = Genre.DRAMA,
            actors = listOf(bradPitt, johnnyDepp)
        )
        ctx.json(listOf(lionKing))
    }
}

When started, you can visit localhost:8080/movies and it will return:

[
  {
    "name": "Lion King",
    "producerName": "Disney",
    "releaseDate": "2019-07-17T15:30:00",
    "genre": "DRAMA",
    "actors": [
      {
        "firstName": "Johnny",
        "lastName": "Depp",
        "dateOfBirth": "1989-07-17"
      },
      {
        "firstName": "Brad",
        "lastName": "Pitt",
        "dateOfBirth": "1990-11-03"
      }
    ]
  }
]

Generating Typings

Now we have some DTO’s and a working REST API, it’s time to generate typings. There are multiple ways of doing this. One option is to use a gradle/maven plugin like github.com/vojtechhabarta/typescript-generator

However for this example I chose to use github.com/ntrrgc/ts-generator and write some Kotlin code to generate the typings. Using code to generate typings allows me rename or move types safely in my IDE. It also gives me more flexibility as I can add my own code after the generation if I so desire.

The code:

val typingsContent = TypeScriptGenerator(
    rootClasses = setOf(
        MovieDto::class
    ),
    mappings = mapOf(
        LocalDate::class to "Date",
        LocalDateTime::class to "Date"
    )
).definitionsText

The output:

interface ActorDto {
    dateOfBirth: Date | null;
    firstName: string;
    lastName: string;
}

type Genre = "ACTION" | "COMEDY" | "THRILLER" | "HORROR" | "DRAMA";

interface MovieDto {
    actors: ActorDto[];
    genre: Genre;
    name: string;
    producerName: string;
    releaseDate: Date;
}

Publishing an NPM package

Now we have typings, all we need to do next is create an NPM package and publish it. Normally expect you to have a local registry for this or a company scope in NPM. For now I will just publish them as a public NPM package with the name backend-api-as-npm-typings-blog.

All we need is a separate directory with a package.json and the typings generated for the previous step.

{
  "name": "backend-api-as-npm-typings-blog",
  "version": "0.0.12",
  "description": "typings for backend-api-npm-typings java app",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/toefel18/backend-api-as-npm-typings-blog.git"
  },
  "author": "toefel18@gmail.com",
  "license": "ISC",
  "homepage": "https://github.com/toefel18/backend-api-as-npm-typings-blog#readme"
}

The version should be kept in-sync with the project version of your backend API. Gradle or maven knows this version. I again used Kotlin code to inject the version in a package.json and gradle to execute this code while injecting the required variables.

This is the complete Kotlin code that generates the typings.d.ts and package.json

object TypingsGenerator {
    @JvmStatic
    fun main(args: Array<String>) {
        val typingsDestination = "${args[0]}/src/typings.d.ts"
        val packageJsonDestination = "${args[0]}/package.json"
        val projectVersion = args[1]

        val typingsContent = TypeScriptGenerator(
            rootClasses = setOf(
                MovieDto::class
            ),
            mappings = mapOf(
                LocalDate::class to "Date",
                LocalDateTime::class to "Date"
            )
        ).definitionsText
        File(typingsDestination).writeText(typingsContent)

        val packageJsonContent = packageJson.replace("PROJECT_VERSION", projectVersion)
        File(packageJsonDestination).writeText(packageJsonContent)
    }

    val packageJson = """{
  "name": "backend-api-as-npm-typings-blog",
  "version": "PROJECT_VERSION",
  "description": "typings for backend-api-npm-typings java app",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/toefel18/backend-api-as-npm-typings-blog.git"
  },
  "author": "toefel18@gmail.com",
  "license": "ISC",
  "homepage": "https://github.com/toefel18/backend-api-as-npm-typings-blog#readme"
}
"""
}

Inside build.gradle I added a custom task to execute this code:

task(generateTypings, dependsOn: 'classes', type: JavaExec) {
    main = 'nl.toefel.blog.backendtypings.dto.typings.TypingsGenerator'
    classpath = sourceSets.main.runtimeClasspath
    args = ["backend-api-typings", "${project.version}"]
}

Now running ./gradlew generateTypings will create a package.json and src/typings.d.ts inside the directory backend-api-typings. The version of the npm package equals the project.version variable inside build.gradle. There are multiple ways to determine a project version, I used this simple one for the sake of the example.

To publish this package, we simply need to run npm publish --access public inside the directory backend-api-typings. --access public is necessary in my case because I do not use a scope or custom registry.

Make sure you have an account on npmjs.org and first run npm login if you try this yourself (also change the package name to avoid conflicts). If you integrate this in a build, use ~/.npmrc to provide the credentials.

To integrate publication inside your gradle build:

task(publishTypings, type: Exec) {
    workingDir 'backend-api-typings'
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine "npm.cmd", "publish", "--access", "public"
    } else {
        commandLine "npm", "publish", "--access", "public"
    }
}

This allows you to run ./gradlew publishTypings.

You can view the published package here: www.npmjs.com/package/backend-api-as-npm-typings-blog.

Using the typings in your Typescript App

I created a simple react app inside the frontend directory using the command npx create-react-app my-app --typescript.

Install the dependency with the typings using npm install --save backend-api-as-npm-typings-blog.

Your Typescript project should include tsconfig.json. If you used create-react-app with the typescript option, it’s already there. Inside that file you find typescript configurations. One of the settings is include. You should add the generated typings there so that Typescript and our IDE pick it up.

// tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve"
  },
  "include": [
    "src",
    "../backend-api-typings/src/typings.d.ts"
  ]
}

Final Results

Now I can fetch a movie from our backend API and interpret the response as a MovieDto. You now have type-safety throughout your app, and your IDE provides code completion on your backend types:

backend typings idea

I hope you enjoyed this post, and any comments or better alternatives are welcome.

JSNation Amsterdam How to make a well informed decision before adding a NPM dependency to your project