DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • The Complete Tutorial on the Top 5 Ways to Query Your Relational Database in JavaScript - Part 2
  • Alexa Skill With Local DynamoDB
  • Schema Change Management Tools: A Practical Overview
  • The First Annual Recap From JPA Buddy

Trending

  • Solid Testing Strategies for Salesforce Releases
  • Ensuring Configuration Consistency Across Global Data Centers
  • Grafana Loki Fundamentals and Architecture
  • Java's Quiet Revolution: Thriving in the Serverless Kubernetes Era
  1. DZone
  2. Coding
  3. Languages
  4. An Introduction to Type Safety in JavaScript With Prisma

An Introduction to Type Safety in JavaScript With Prisma

In this article, we walk you through an example app built with Prisma, Fastify, and MySQL that implements automated checks for increased type safety.

By 
Oleksii Klochai user avatar
Oleksii Klochai
·
Updated Aug. 30, 21 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
9.2K Views

Join the DZone community and get the full member experience.

Join For Free

If you’ve worked with JavaScript long enough, you’ve likely run into type-related issues. For instance, you might have accidentally converted a value from an integer to a string:

JavaScript
 
> console.log("User's cart value:", "500" + 100)
[Log] User's cart value: "500100"
No big deal, right?

A seemingly innocent error can become a problem if it resides in, say, your application’s billing code. And with JavaScript increasingly being used in critical services, such a scenario is likely to happen in real life. Luckily, database tools like Prisma deliver type safety in the database access layer for your JavaScript projects.

In this article, we provide a background on typing in JavaScript and highlight the implications for real-world projects. We then walk you through an example app—built with Prisma, Fastify, and MySQL—that implements automated checks for increased type safety.

Let’s dive in!

Architecture of our example application with Prisma, Fastify, and MySQL

Static vs Dynamic Typing

In different programming languages, the checking of types for variables and values can happen at different stages of the program’s compilation or execution. Languages can also allow or disallow certain operations, as well as allowing or disallowing type combinations.

Depending on when exactly the type checking happens, a programming language can be statically typed or dynamically typed. Statically-typed languages, like C++, Haskell, and Java, all generally check for type errors at compile time.

Dynamically-typed languages, like JavaScript, instead check for type errors during the program’s execution. A type error is not that easy to get in JavaScript as this language’s variables do not have types. But if we try to “accidentally” use a variable as a function, we’ll get a TypeError during the program’s run time:

JavaScript
 
// types.js
numBakers = 2;
console.log(numBakers());

The error looks as follows in our console:

JavaScript
 
TypeError: numBakers is not a function

Strong vs Weak Typing

The other axis of type checking is strong typing vs. weak typing. Here, the line between strong and weak is blurry and is defined depending on a developer’s or a community’s opinion.

Some folks say that a language is weakly-typed if it allows implicit type conversions. In JavaScript, the following code is valid even though we’re calculating a sum of an integer and a string:

JavaScript
 
numBakers = 1;
numWaiters = "many";
totalStaff = numBakers + numWaiters; // 1many

This code gets executed without errors, and the value 1 gets implicitly converted into a string when it comes to calculating the value of totalStaff.

Based on such behavior, JavaScript can be considered a weakly-typed language. But what does weak typing mean in practical terms? 

For many developers, type weakness can cause discomfort and uncertainty when writing JavaScript code, especially when working with sensitive systems, like billing code or accounting data. Unexpected types in variables, if not controlled for, can cause confusion and even produce real damage.

Consider a baking equipment website that allows you to purchase a commercial-grade mixer and some replacement parts for it:

JavaScript
 
// types.js
mixerPrice = "1870";
mixerPartsPrice = 100;
console.log("Total cart value:", mixerPrice + mixerPartsPrice);

Notice that there’s a type mismatch between how the prices are defined. Possibly, an incorrect type previously got sent to the backend, and the information was incorrectly stored in the database as a result. What happens if we execute this code? Let’s run the example to illustrate the result:

Shell
 
$ node types.js
Total cart value: 1870100

Oof! A bakery’s owner might be a little surprised if such a charge were to hit their bank account.

How JavaScript's Lack of Type Safety Can Slow You Down

To avoid situations like the one with the shopping cart above, developers seek type safety—  it guarantees that the data they operate on is of a certain type and will result in predictable behavior.

As a weakly-typed language, JavaScript does not provide type safety. Still, many production systems that handle bank balances, insurance amounts, and other sensitive data are developed in JavaScript.

Developers are wary of unexpected behavior not only because it can lead to incorrect transaction amounts. The lack of type safety in JavaScript can be an inconvenience for a variety of other reasons such as:

  • Reduction of productivity:  If you have to deal with type errors, debugging them and thinking through all the possibilities of type interactions going wrong can take a long time and add mental overhead.
  • Boilerplate code for handling type mismatches: In type-sensitive operations, developers frequently need to add code to check for types and reconcile any possible differences. In addition, engineers have to write many tests for the handling of the undefined data type. Adding extra code that isn’t directly related to your application’s business value is not ideal for keeping your codebase readable and clean.
  • Lack of clear error messages:  Sometimes type mismatches can generate cryptic errors way down the line from the location of the type error. In such situations, type errors can be frustrating and difficult to debug.
  • Unexpected issues when writing to databases:  Type errors can cause issues when it comes to writing to your database. For example, as an application’s database evolves, developers frequently need to add new database fields. Adding a field in a staging environment but forgetting to roll it out to the production environment can lead to unexpected type errors as the production deployment goes live.

Since type errors in the database layer of your application can cause lots of harm due to data corruption, developers have to come up with solutions for the problems that lack of type safety introduces. In the next section, we discuss how introducing a tool like Prisma can help you address type safety in your JavaScript projects.

Type-Safe Database Access Using Prisma

While JavaScript itself does not offer built-in type safety, Prisma allows you to opt into type safety checks in your application. Prisma is a new kind of ORM tool that consists of a type-safe query builder for JavaScript and TypeScript (Prisma Client), a migration system (Prisma Migrate), and a GUI for interacting with your database (Prisma Studio).

At the core of the type checks in Prisma is the Prisma schema, the single source of truth where you model your data. Here’s what a minimal schema looks like:

JavaScript
 
// prisma/schema.prisma
model Baker {
  id Int @id @default(autoincrement())
  email String @unique
  name String?
}

In this example, the schema describes a Baker entity where each instance, an individual baker, has an email (a string), a name (also a string, optional), and an automatically incremented identifier (an integer). The word “model” is used to describe a data entity that is mapped to a database table on the backend.

Under the hood, the Prisma CLI generates Prisma Client from your Prisma schema. The generated code allows you to access your database conveniently in JavaScript, and implements a series of checks and utilities to make your code type-safe. Each model that is defined in the Prisma schema gets converted into a JavaScript class with functions for accessing individual records.

By using a tool like Prisma in your project, you start taking advantage of the additional type checks when using the data types generated by the library (its object-relational mapping layer, or ORM) to access records in your database.

Example of Implementing Type Safety in a Fastify App

Let’s look at an example of using a Prisma schema in a Fastify application. Fastify is a web framework for Node.js that focuses on performance and simplicity.

We’ll use the prisma-fastify-bakery project, which implements a simple system for tracking a bakery’s operations.

Preliminary Setup

To run the project, we’ll need to have a recent Node.js version set up on our development machine. The first step is to clone the repo and install all required dependencies:

Shell
 
$ git pull https://github.com/chief-wizard/prisma-fastify-bakery.git
$ cd prisma-fastify-bakery
$ npm install

We’ll also need to make sure that we have a MySQL server running. If you need help installing and setting up MySQL, check out Prisma’s guide on the topic.

To document where the database can be reached, we’ll create a .env file in the root of our repository:

Shell
 
$ touch .env

Now, we can add the database URL to the .env file. Here’s how an example file looks:

Shell
 
DATABASE_URL='mysql://root:bakingbread@localhost/mydb?schema=public'

With the setup completed, let’s move on to the step where we create our Prisma schema.

Creating a Schema

The first step on our way to type safety is to add a schema. In our prisma/schema.prisma file, we define the data source, which in this case is our MySQL database. Note that rather than hard-coding our database credentials in our schema file, we read the database URL from a .env file. Reading in sensitive data from the environment is safer in terms of security:

JavaScript
 
datasource db {
  provider = "mysql"
  url = env("DATABASE_URL")
}

We then define the types that are relevant for our application. In our case, let’s look at the model for the products that we’ll sell at the bakery. We want to record items like baguettes and croissants, and also make it possible to track items like bags of coffee and bottles of juice. Items will have types like “pastry,” “bread,” or “coffee”, and categories like “sweet” or “savory,” where applicable. We’ll also store references to sales for each product, as well as the products’ prices and ingredients.

In the Prisma schema file, we start by naming our Product model:

JavaScript
 
model Product {
...
}

We can add an id property—this will help us quickly identify each record in the products table and will serve as the index:

JavaScript
 
model Product {
  ...
  id          Int @id @default(autoincrement())
  ...
}

We can then add all the other properties that we’d like each item to contain. Here, we’d like the name of each item to be unique, giving us only one entry for each product. To refer to ingredients and sales, we use Ingredient and Sale types, which we define separately:

JavaScript
 
model Product {
  ...
  name        String @unique
  type        String
  category    String
  ingredients Ingredient[]
  sales       Sale[]
  price       Float
  ...
}

We now have a complete model for our products in our Prisma schema. Here is how the prisma.schema file looks like, including the Ingredient and Sale models:

JavaScript
 
model Product {
  id          Int @id @default(autoincrement()) 
  name        String @unique
  type        String
  category    String
  ingredients Ingredient[]
  sales       Sale[]
  price       Float
}


model Ingredient {
  id          Int @id @default(autoincrement()) 
  name        String @unique
  allergen    Boolean
  vegan       Boolean
  vegetarian  Boolean
  products    Product? @relation(fields: [products_id], references: [id])
  products_id Int?
}

model Sale {
  id          Int @id @default(autoincrement()) 
  date        DateTime @default(now())
  item        Product? @relation(fields: [item_id], references: [id])
  item_id     Int?
}

To translate our model into live database tables, we instruct Prisma to run a migration. A migration contains the SQL code to create the tables, indices, and foreign keys in the database. We also pass in the desired name for this migration, init, which stands for “initial migration”:

Shell
 
$ npx prisma migrate dev --name init

We see the following output indicating that the database has been created in accordance with our schema:

Shell
 
MySQL database mydb created at localhost:3306

The following migration(s) have been applied:

migrations/
└─ 20210619135805_init/
└─ migration.sql
...

Your database is now in sync with your schema.

✔ Generated Prisma Client (2.25.0) to ./node_modules/@prisma/client in 468ms

At this point, we're ready to use the objects defined by our schema in our application.

Creating a REST API That Uses Our Prisma Schema

In this section, we’ll start to use the types from the Prisma schema and thus set the stage for implementing type safety. Jump straight to the next section if you’d like to see type-safety checks in action.

Since we’re using Fastify for our example, we create a product.js file under the fastify/routes directory. We add the products model from our Prisma schema, as follows:

JavaScript
 
const { PrismaClient } = require("@prisma/client")
const { products } = new PrismaClient()

We can then define a Fastify route that uses the Prisma-provided findMany function on the model. We pass the parameter take: 100 to the query to limit the results to a maximum of 100 items, to avoid overloading our API:

JavaScript
 
async function routes (fastify, options) {

    fastify.get('/products', async (req, res) => {
        const list = await product.findMany({
            take: 100,
        })   
        res.send(list)
    })
    ...
}

The real value of type safety comes into play when we try to add a creation endpoint for our bakery products. Normally, we would need to check every input for its type. But in our example, we can skip the checks completely because Prisma Client will run them through the schema first:

JavaScript
 
...
// create
    fastify.post('/product/create', async (req, res) => {

        let addProduct = req.body;
        const productExists = await product.findUnique({
            where: {
                name: addProduct.name
            }
        })

        if(!productExists){
            let newProduct = await product.create({
                data: {
                    name: addProduct.name,
                    type: addProduct.type,
                    category: addProduct.category,
                    sales: addProduct.sales,
                    price: addProduct.price,
                },
            })
            res.send(newProduct);
        } else {
            res.code(400).send({message: 'record already exists'})            
        }
    })
...

In the example above, we go through the following steps in the /product/create endpoint:

  • Assign the body of the request to the variable addProduct. The variable contains all the details that were supplied in the request.

  • Use the findUnique function to find out whether we already have a product with the same name. The where clause allows us to filter the results to only include products with the name that we have supplied. If productExists variable is non-empty after running this query, then we already have an existing product with the same name.

  • If the product does not exist:

    • We create it with all the fields that were received in the requests. We do so by using the product.create function where the details of the new product are located under the data section.

  • If the product already exists, we return an error.

As the next step, let’s test the /product and /product/create endpoints using cURL.

Populating the Database Using Prisma Studio and Testing Our API

We can start our development server by running the following command:

Shell
 
$ npm run dev

Let’s open Prisma Studio and have a look at what’s currently inside our database. We’ll run the following command to launch Prisma Studio:

Shell
 
$ npx prisma studio

Once it’s launched, we’ll see the different models we have in our application and the number of records each one has over at a local URL http://localhost:5555:

Prisma Studio interface

There are currently no entries under the Product model, so let's create a couple of records by clicking  the "Add new record" button:

Adding products in the Prisma Studio interface

With these data points added, let’s test our products endpoint using the following cURL command:

Shell
 
$ curl localhost:3000/products
# output

[{"id":1,"name":"baguette","type":"savory","category":"bread","price":3,"ingredients":[]},{"id":2,"name":"white bread roll","type":"savory","category":"bread","price":2,"ingredients":[]}]

Easy enough! Let’s create another product through our product creation API:

Shell
 
$ curl -X POST -H 'Content-Type: application/json' -d '{"name": "rye bread roll", "type":"savory", "category":"bread", "price": 2}' localhost:3000/product/create

# output

{"id":3,"name":"rye bread roll","type":"savory","category":"bread","price":2,"ingredients":[]}

Another item successfully created! Next, let’s see how our example does when it comes to type safety.

Trying Out Type Safety in Our API

Remember that we’re currently not checking the contents of the request on our product creation endpoint. What happens if we incorrectly specify the price using a string instead of a floating-point number? Let’s find out:

Shell
 
$ curl -X POST -H 'Content-Type: application/json' -d '{"name": "whole wheat bread roll", "type":"savory", "category":"bread", "price": "1.50"}' localhost:3000/product/create

# output

{"statusCode":500,"error":"Internal Server Error","message":"\nInvalid `prisma.product.create()` invocation:\n\n{\n  data: {\n    name: 'whole wheat bread roll',\n    type: 'savory',\n    category: 'bread',\n    sales: undefined,\n    price: '1.50',\n           ~~~~~~\n    ingredients: {\n      connect: undefined\n    }\n  },\n  include: {\n    ingredients: true\n  }\n}\n\nArgument price: Got invalid value '1.50' on prisma.createOneProduct. Provided String, expected Float.\n\n"}

As you can see, the Prisma checks have prevented us from creating an item with incorrect pricing—without our having to add any explicit checks for this particular case!

Tips for Adding Type Safety for an Existing Project

By this point, we’re well aware of the value that type checks can add to a JavaScript project. If you’d like to try adding such checks to your existing project, here are a few tips to help you get started.

Introspect a Database To Generate an Initial Schema

When using Prisma, database introspection allows you to look at the current layout of tables in a database and generate a fresh schema from the information you already have. This feature is a helpful starting point if you don’t feel like writing the schema by hand.

Try running npx prisma introspect, and a new schema.prisma file will be automatically generated in your project directory in only a few seconds.

Type Checks in VS Code

If Visual Studio Code is your programming environment of choice, you can take advantage of the ts-check directive to get type-checking suggestions directly in your code. In your JavaScript files that use the Prisma Client, add the following comment at the top of each file:

JavaScript
 
// @ts-check

With this check enabled, if we try to make a type error, we’ll get an immediate warning from VS Code:

Visual Studio Code type check in actionThe highlighting of type errors makes it easier to spot type-related issues early on. Learn more about this functionality in this Productive Development with Prisma article.

Type Checks in Your Continuous Integration Environment

The trick above with @ts-check works because Visual Studio Code runs your JavaScript file through the TypeScript compiler. You can also run the TypeScript compiler directly, e.g., in your continuous integration environment. Adding type checks on a file-by-file basis can be a viable way to kickstart your type safety efforts.

To start checking types in a file, add the TypeScript compiler as a development dependency:

Shell
 
$ npm install typescript --save-dev

With the dependency installed, you can now run the compiler on JavaScript files, and the compiler will issue warnings if anything seems off. We recommend running TypeScript checks on one or a few files to start:

JavaScript
 
$ npx tsc --noEmit --allowJs --checkJs fastify/routes/product.js
The example above will run the TypeScript compiler on our Fastify product routes file.

Learn More About Implementing Type Safety in JavaScript

Ready to bake some type-safe code into your own codebase? Check out our complete code example in the prisma-fastify-bakery repository and try running the project yourself.

If you get stuck, Prisma has a helpful Slack community that you can join. It’s a great place to ask questions if you need help along the way.

Database JavaScript Type safety Schema code style application Data (computing) shell Visual Studio Code

Opinions expressed by DZone contributors are their own.

Related

  • The Complete Tutorial on the Top 5 Ways to Query Your Relational Database in JavaScript - Part 2
  • Alexa Skill With Local DynamoDB
  • Schema Change Management Tools: A Practical Overview
  • The First Annual Recap From JPA Buddy

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!