Publish your backend API typings as an NPM package
In this post I suggest a way to publish your backend API typings as an NPM package. front-end 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 front-end, we lose the types. In this post I suggest a way to publish your backend API types as an NPM package. The front-end 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 front-end 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
```kotlin
object TypingsGenerator {
@JvmStatic
fun main(args: Array
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 front-end
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.
{
"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:

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