January 19, 2021

Full Stack Type Safety with Angular, Nest, Nx, and Prisma

TypeScript really shines when we can extend type safety to all parts of our stack. In this article, we'll look at how to apply type safety to every part of a full stack Angular and NestJS app, including database access. We'll see how to share types across the stack by using an Nx monorepo.

2020 was a great year for TypeScript. Usage surged, developers came to love the benefits of type safety, and the language saw adoption in many different settings.

While TypeScript might be fairly new to developers who mainly work with React, Vue, Svelte, and others, it has been around for quite some time for Angular developers. Angular (version 2+) was initially authored in 2015 and was done so using TypeScript. The Angular team made an early bet on type safety, encouraging developers to write their Angular apps in TypeScript as well, even though writing them in JavaScript was an option.

Many Angular developers were initially resistant. TypeScript wasn't very mature in 2015 and there was a steep learning curve. It was common to be slowed down by environment incompatabilities and bugs. All around, there was often a great deal of frustration.

Fast-forward to 2021 and Angular developers have been enormously successful using TypeScript. Teams have benefited greatly from type safety over the years.

While type safety for Angular applications is nothing new, it's less common for Angular developers who work across the full stack. Frameworks like NestJS have made it easy to use TypeScript in a Node environment, but one spot that has continued to lack is at the database. Several tools now exist for acheiving type-safe database access, Prisma being one of them.

In this article, we'll look at how we can use the types generated by Prisma to apply type safety to all parts of an Angular and Nest ecommerce application. We'll work in an Nx monorepo so that we can easily import type across the whole stack. Let's get started!

Check out the code for the project on GitHub.

Products from ShirtShop

Create an Nx Workspace

One of the easiest ways to share types between a front end and backend project is to house everything under a monorepo. Nx Dev Tools (created by Nrwl) makes working with monorepos simple. Nx stipulates a set of conventions that, when followed, allow for simplicity when maintaining multiple applications under a single repository.

Let's start by creating an Nx workspace for our project. We'll use the create-nx-workspace command to do so.

In a terminal window, create a workspace with a preset of angular.

npx create-nx-workspace --preset=angular

An interactive prompt takes us through the setup process. Select a name for the workspace and application and then continue through the prompts.

Interactive prompts for setting up an Nx workspace

Once Nx finishes wiring up the workspace, open it up and try running the Angular application.

npm start

This command will tell Nx to serve the Angular application that was created as the workspace initialized. After it compiles, open up localhost:4200 to make sure everything is working.

The Angular application running on localhost:4200

Add a NestJS Application

Our front end is ready to go but we haven't yet included a project for the backend. Let's add a NestJS project to the workspace.

To add our NestJS project, we first need to install the official NestJS plugin for Nx. In a new terminal window, grab the @nrwl/nx package from npm.

npm install -D @nrwl/nest

After installation, use the plugin to generate a NestJS project within the workspace. Since we'll only have one backend project for this example, let's just name it "api".

nx generate @nrwl/nest:application api

Once the generator finishes, we can see a new folder called api under the apps directory. This is where our NestJS app lives.

The default NestJS installation comes with a single endpoint which returns a "hello world" message. Let's start the API and make sure we can access the endpoint. To start the API, target the nx serve command directly at the NestJS app.

nx serve api

Once the API is up and running, go to http://localhost:3333/api in the browser and make sure you can see the "hello world" message.

The NestJS application running on localhost:3333/api

Install Prisma and Set Up a Database

Now that we've got our front end and backend projects in place, let's set up Prisma so we can start writing some code!

We need to install two packages to work with Prisma: the Prisma Client (as a regular dependency) and the Prisma CLI (as a dev dependency).

npm install @prisma/client
npm install -D @prisma/cli

The Prisma Client is what gives us ORM-style type-safe database access in our code. The Prisma CLI is what gives us a set of commands to initialize Prisma, create database migrations, and more.

With those packages installed, let's initialize Prisma.

npx prisma init

After running this command, a prisma directory is created at the workspace root. Inside is a single file called schema.prisma.

This file uses the Prisma Schema Language and is the place where we define the shape of our database. We use it to describe the tables for our databases and their columns, the relationships between tables, and more.

When we create a Prisma model, we need to select a provider for our datasource. The default schema.prisma file comes with a datasource called db which uses PostgreSQL as the provider.

Instead of using Postgres, let's use SQLite so we can keep things simple. Switch up the db datasource so that uses SQLite. Point the url parameter to a file called dev.db within the filesystem.

datasource db {
provider = "sqlite"
url = "file:./dev.db"
}

Note: We don't need to create the dev.db file ourselves. Its creation will be taken care of for us in a later step.

Let's now set up a simple model for our shop. To get ourselves started, let's work with a single table called Product. To do so, create a new model in the schema file and give it some fields.

model Product {
id String @id @default(cuid())
name String
description String
image String
price Int
sku String
}

The id field is marked as the primary key via the @id directive. We're also setting its default value to be a collision-resistant unique ID. The other fields and fairly straight-forward in their purpose.

With the model in place, let's run our first migration so that the filesystem database file gets created and populated with our Product table.

npx prisma migrate dev --preview-feature

An interactive prompt will ask for the name of the migration. Call it whatever you like, something like init works fine.

After the migration completes, a dev.db file is created in the prisma directory, along with a migrations directory. It's within the migrations directory that all of the SQL that's used to perform our database migrations is stored. Since these files are raw SQL, we have the opportunity to adjust them before they operate on our databases. Read the migrate docs to find out more about how you can customize the migration behavior.

View the Database with Prisma Studio and Seed Some Data

With the database in place and populated with a table, we can now take a look at it and add some data using Prisma Studio. Prisma Studio is a GUI for viewing and managing our databases and is available in-browser or via a desktop app.

In a new terminal window, use the Prisma CLI to fire up Prisma Studio.

npx prisma studio

Running this command will open Prisma Studio. In the browser, it opens at localhost:5555.

Prisma Studio running at localhost:5555

We can use Prisma Studio to add data to the database manually. This isn't a great approach if we have a lot of data to seed, but it's useful if we want to add a few records to test with.

Add as many rows as you like and input data for them. If you would like to work with the data seen in this article, you can grab it in this gist.

New rows in Prisma Studio

Next, save the changes. IDs for each row will automatically be generated.

Saved data in Prisma Studio

We now have all the pieces of our stack in place! We're ready to start writing some code to surface the data from the API and call for it from the Angular app.

Create a Products Controller for the API

The data in our database is ready to go. What we need now is an endpoint we can call to retreive it. To make this happen, we'll create a library for our NestJS controller and a service that we can reach into to expose an endpoint that responds to GET requests.

Use the NestJS Nx plugin to generate a new library called products. Include a controller and a service within.

nx generate @nrwl/nest:library products --controller --service

We'll create a method in the service to reach into our database to get the data. Then, in the controller, we'll expose a GET endpoint which uses the service to get that data and return it to the client.

Let's start by building out the database query within the service. This is the first spot we'll see Prisma's types really shine!

Within products.service.ts, import PrismaClient, create an instance of it, and expose a public method to query for the data.

// libs/products/src/lib/products.service.ts
import { Injectable } from '@nestjs/common'
import { PrismaClient, Product } from '@prisma/client'
const prisma = new PrismaClient()
@Injectable()
export class ProductService {
public getProducts(): Promise<Product[]> {
return prisma.product.findMany()
}
}

We're importing two things from @prisma/client here: PrismaClient and Product.

PrismaClient is what we use to create an instance of our database client and it exposes methods and properties that are useful for querying the database.

The Product import is the TypeScript type that was generated for us by Prisma when we ran our database migrations. This type has the shape of our Product table and is useful for informing consumers of the getProducts method about what it can expect the returned data to look like.

Note: We're instantiating PrismaClient directly within our ProductsService file here. In a real world application, we should instead create a dedicated file for this instance. That way, we wouldn't need to instantiate it multiple times.

Let's now work within the controller to make a call to getProducts to fetch the data. Open up products.controller.ts and add a method which responds to GET requests.

// libs/products/src/lib/products.controller.ts
import { Controller, Get } from '@nestjs/common'
import { ProductsService } from './products.service'
@Controller('products')
export class ProductController {
constructor(private productService: ProductsService) {}
@Get()
public getProducts() {
return this.productService.getProducts()
}
}

We've applied the getProducts method with the @Get decorator which means when we make a GET request to /products, the method will be run. The method itself reaches into the service to get the data.

Before we can test out this endpoint, we need to add ProductsController and ProductsService in the main module for the api.

Open up app.module.ts found within apps/api/src/app and import ProductsController and ProductsService. Then include them in the controllers and providers arrays respectively.

// apps/api/src/app/app.module.ts
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { ProductsController, ProductsService } from '@shirt-shop/products'
@Module({
imports: [],
controllers: [AppController, ProductsController],
providers: [AppService, ProductsService],
})
export class AppModule {}

Now head over to the browser and test it out by going to http://localhost:3333/api/products.

Products data from the API

It may not be very apparent at this point, but our endpoint has a layer of type safety applied to it that can help us out if we need to manipulate and/or modify data before it is returned to the client. For example, if we need to map over our data and get access to its properties, we now have full autocompletion enabled when we do so. This occurs because we told the getProducts method in the ProductsService that the return type is a Promise that resolves with an array of type Product.

Autocompletion on the Product type

Now that we have the API working, let's wire up the Angular application to make a call for this data and display it!

Enable CORS

When we create our NestJS API, we have the option of setting up a proxy for our frontend applications such that both the front end and backend get served over the same port. This is useful for situations where we don't want to have separate domains for the two sides of the app.

Instead of setting up a proxy for this demo, we can instead enable CORS on the backend so that our front end can make calls to it. We won't need this until later, but let's get it set up and out of the way now.

Open up apps/api/src/main.ts and add a call to `app.enableCors();

// apps/api/src/main.ts
import { Logger } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app/app.module'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
const globalPrefix = 'api'
app.setGlobalPrefix(globalPrefix)
app.enableCors()
const port = process.env.PORT || 3333
await app.listen(port, () => {
Logger.log('Listening at http://localhost:' + port + '/' + globalPrefix)
})
}
bootstrap()

Create a UI Module for the Angular App

We could just start building components directly within the shirt-shop app in our Nx workspace, but that would be against the advice that Nx gives about how to manage code in our monorepos. Instead, let's create a new module that will be dedicated to components that make up our UI.

Head over to the command line and create a new module. Follow the prompts to select the desired CSS variety.

nx generate @nrwl/angular:lib ui

Once the module is in place, we can create a component to list our products as well as a service to make the API call to get the data.

Let's start by generating a component.

nx g component products --project=ui --export

Using the --project=ui flag tells Nx that we want to put this component in our newly-created ui module. We can see the result under /libs/ui/src/lib/products.

Let's now create a service.

nx g service product --project=ui --export

With the new UiModule in place, we now need to add it to the imports array in our app.module.ts file for the frontend.

// apps/shirt-shop/src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser'
import { NgModule } from '@angular/core'
import { AppComponent } from './app.component'
import { UiModule } from '@shirt-shop/ui'
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, UiModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

Note: If you get any errors saying that @shirt-shop/ui cannot be found, try restarting the front end by stopping that process and running nx serve again.

Add an API Call to the Service

We'll use Angular's built-in HttpClientModule to get access to an HTTP client for making requests to the API. To get started, let's import the appropriate module. The place to do this is within the ui.module.ts file in our new UiModule.

// libs/ui/src/lib/ui.module.ts
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { ProductsComponent } from './products/products.component'
import { HttpClientModule } from '@angular/common/http'
@NgModule({
imports: [CommonModule, HttpClientModule],
declarations: [ProductsComponent],
exports: [ProductsComponent],
})
export class UiModule {}

We can now import Angular's HttpClient within our ProductService and make calls with it.

// libs/ui/src/lib/product.service.ts
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Product } from '@prisma/client'
import { Observable } from 'rxjs'
@Injectable({
providedIn: 'root',
})
export class ProductService {
private API_URL: string = 'http://localhost:3333/api'
constructor(private readonly http: HttpClient) {}
public getProducts(): Observable<Product[]> {
{
return this.http.get<Product[]>(`${this.API_URL}/products`)
}
}
}

Notice that we're using the same Product type that gets exported from @prisma/client here within our ProductService that was used on the backend in the ProductsController. This is a great illustration of how we can benefit from using the same types across our whole stack. When we use the getProducts method from this service, we'll now have type safety applied.

Build Out the Products Component

We're now ready to add some structure and style to our ProductsComponent so we can display the products to our users.

Let's start by adding some CSS that will style our component.

Open up libs/ui/src/lib/products/product.component.css and add the following styles:

/* libs/ui/src/lib/products/product.component.css */
:host {
display: grid;
gap: 40px;
grid-template-columns: repeat(3, 33% [col-start]);
}
.product-card {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
background-color: #fff;
border-radius: 15px;
padding: 15px;
}
.product-card img {
border-radius: 15px;
max-width: 100%;
height: 200px;
display: block;
margin: 0 auto;
}
.product-name {
font-weight: bold;
font-size: 22px;
}
.product-description {
color: rgb(122, 122, 122);
}
.product-price {
font-weight: bold;
font-size: 24px;
}
.add-to-cart-button {
background: rgb(49, 175, 255);
background: linear-gradient(90deg, rgba(49, 175, 255, 1) 0%, rgba(0, 123, 252, 1) 100%);
padding: 10px 20px;
border-radius: 30px;
border: none;
color: rgb(219, 233, 248);
cursor: pointer;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
}

Next, open up libs/ui/src/lib/products/product.component.html and add the structure for products to be displayed..

<section class="product-listings" *ngFor="let product of $products | async">
<div class="product-card">
<img [src]="product.image" />
<p class="product-name">{{ product.name }}</p>
<p class="product-description">{{ product.description }}</p>
<p class="product-price">{{ product.price | currency }}</p>
<button class="add-to-cart-button">Add to Cart</button>
</div>
</section>

Finally, we need to add a method to the component class which uses the ProductService to get the data. We'll then put the result on the $products observable that we've already stubbed out in our template above.

// libs/ui/src/lib/products/products.component.ts
import { Component, OnInit } from '@angular/core'
import { ProductService } from '../product.service'
import { Observable } from 'rxjs'
import { Product } from '@prisma/client'
@Component({
selector: 'shirt-shop-products',
templateUrl: './products.component.html',
styleUrls: ['./products.component.css'],
})
export class ProductsComponent implements OnInit {
public $products: Observable<Product[]>
constructor(public productService: ProductService) {}
ngOnInit(): void {
this.$products = this.productService.getProducts()
}
}

This is another spot where we're using our Product type from @prisma/client to give ourselves type safety. Applying this type directly to the $products observable means that we can get autocompletion in our Angular templates.

Autocompletion in the Angular template

With our component in place, we're now ready to call it from the shirt-shop app and display the results!

Open up apps/shirt-shop/src/app/app.component.html and include the Products component.

<h1>Welcome to Shirt Shop!</h1>
<shirt-shop-products></shirt-shop-products>

Products from ShirtShop

Going Beyond Displaying Data

For any real-world applicaton, we no doubt need a way to take user input and create records in the database.

We won't build out a full CRUD experience for this demonstration, but we can take a quick look at some of the features from the PrismaClient that would help us store new data.

Let's say we have a section in our app which allows admins to add new products in. We'd likely want to start by creating an endpoint to receive this data and store it. In this case, we could use the create method on PrismaClient along with the ProductCreateInput type that is exposed on a top-level export called Prisma.

import { Injectable } from '@nestjs/common'
import { PrismaClient, Product, Prisma } from '@prisma/client'
const prisma = new PrismaClient()
@Injectable()
export class ProductService {
// ...
public createProduct(data: Prisma.ProductCreateInput): Promise<Product> {
return prisma.product.create({
data,
})
}
}

The createProduct method takes in some data which is type-hinted to abide by the Product model from our Prisma schema. The returned result is a single Product that gets resolved from a Promise.

It should be noted that just type-hinting our data parameter here doesn't do anything to add real validation to this endpoint. For data validation at the endpoint, we need to use Validation Pipes from NestJS.

Wrapping Up

TypeScript has come a long way since its early days and early adoption in the Angular community. Using TypeScript on both the frontend and backend bodes well for developer experience and confidence. Applying type safety to database access goes one step further in providing teams large and small with a slew of benefits. Wrapping the whole application up in a monorepo like those provided by Nx gives us an easy way of reusing code (including type definitions) across the whole stack.

If you'd like to go even further with Prisma, check out the docs, follow us on Twitter, and join our Slack community!

Don’t miss the next post!

Sign up for the Prisma Newsletter

Key takeaways from the Discover Data DX virtual event

December 13, 2023

Explore the insights from the Discover Data DX virtual event held on December 7th, 2023. The event brought together industry leaders to discuss the significance and principles of the emerging Data DX category.

Prisma Accelerate now in General Availability

October 26, 2023

Now in General Availability: Dive into Prisma Accelerate, enhancing global database connections with connection pooling and edge caching for fast data access.

Support for Serverless Database Drivers in Prisma ORM Is Now in Preview

October 06, 2023

Prisma is releasing Preview support for serverless database drivers from Neon and PlanetScale. This feature allows Prisma users to leverage the existing database drivers for communication with their database without long-lived TCP connections!