tldr;
I'm a big fan of using Nx and having multiple apps in a single workspace, especially when the apps have a lot in common and a lot of code to share. This is especially helpful when dealing with interfaces for data retrieved from or sent to the server. In this article, we'll talk about how to share those interfaces between the front and back end.
This isn't the only way to manage these interfaces, but it is working well for us on a full stack project in our Nx workspace. An update can be made to the interface in one spot, and the front and back ends stay in sync. I've worked on a lot of projects where the communication breaks down and it's unclear what data should be sent or expected. That stumbling block can be eliminated by having your front and back end apps in the same workspace.
Setup
If you want to follow along, create a new Nx workspace:
$ npx create-nx-workspace
Give the workspace a name, and then select the angular-nest option for creating the workspace. After that you can enter what you want for the remainder of the prompts. Once the dependencies are installed, you should be good to go.
When you open the project, you'll see a couple directories inside the apps directory. The three directories are api
, your Angular app folder, and the Angular app's end to end test folder. In the libs directory is an api-interfaces
lib.
Creating an Interface
Let's create our first interface. It'll be a simple example, one that's frequently used: a todo. In the api-interfaces
lib, create a new file called todo.interface.ts
next to the api-interfaces.ts
file that was created automatically when the workspace was created. Put the following contents in the new file:
// todo.interface.ts
export interface Todo {
id: number
title: string;
complete: boolean;
}
This will be the base interface for the todos in our app. The Angular app will use this interface for type checking, and the API will implement this interface for the entities and DTOs that are used in NestJS.
Creating a DTO
Now that we have an interface, let's create a DTO that our NestJS app can use to define the data that should be sent when creating a new todo or updating an existing one. In the api
app, create a new file: todo.dto.ts
and add the following:
// create-todo.dto.ts
import { Todo } from '@my-workspace/api-interfaces
export class CreateTodoDto implements Omit<Todo, 'id'> {
title: string;
completed: boolean;
}
Note: in all likelihood you will not create this DTO in the
api
app, but in a new lib that you create for this NestJS module. To keep things simple for this demo, though, I selected theapi
folder to contain the DTOs and entities.
There are a couple things I want to point out here. First, we import our Todo
interface from the api-interfaces
lib, the one we created in the last section. This is the base for the DTO. We implement
the interface to create our CreateTodoDto
, but we use the Omit
utility type to remove the id
attribute from the DTO. The reason for this is because we won't have an id
for the todo when we're creating it; that will be determined by the database. By using the Omit
utility type and removing the id
attribute from the DTO, we don't need to pass a null
id
when creating a new todo while at the same time requiring all the other attributes.
With this DTO now created, we can tell the controller what type to expect when a new todo is created. If an object with more or less attributes is passed to the endpoint, we can return a 400 with the required fields on the object.
Creating an Entity
Creating the entity is similar to the DTO, but we won't need to use the Omit
utility type. Here's an example of the entity:
// todo.entity.ts
import { Todo } from '@my-workspace/api-interfaces';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('todo')
export class TodoEntity implements Todo {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
completed: boolean;
}
The implementation here is very similar to the DTO, but we don't leave out the id
. Instead, we mark it as the primary key and set it to be auto generated when a new todo is created. The other difference is the @Entity()
decorator. This is required by typeorm
to make this a table in the database where todos can be saved. Also, because the class is defined as TodoEntity
, the table would be called todo_entity
by default. By providing a string inside the parentheses for the Entity
decorator, we can set the table name to todo
.
I like to call the entity TodoEntity
so that it's clear when I'm using it in different files that I am not using the interface. There are other ways, however, to distinguish between the two files and objects. Do whatever feels best to you!
The Benefits
So we just created three files for todos in our app. What's the benefit? Well, Angular is more powerful when types or interfaces or classes are used to determine how the data in your app is organized. You get auto completion from your IDE, and you're less likely to use the wrong attribute or method. In addition, with NestJS as our backend, we can easily check that the data that's sent to the backend is shaped correctly so that we don't have server errors popping up because too many attributes (or not enough) are sent along with the request.
The hard part is when you want or need these files on both the front end and the back end. Many times, those applications are in different repositories and managed by different teams. By having both applications in the same workspace, we've closed that gap. And, by having the DTO and entity implement the interface, we're assured that if the interface changes then changes will need to be made to the DTO and entity. Those two files will have errors if attributes are added or removed to the interface and they don't implement the right attributes. Thus, the shape of the data is always the same on the front and back end.
Conclusion
I hope this article helped you with knowing how you can manage the interfaces, DTOs and entities in your application. When starting this project, I was unsure of the best way to do this same thing. I got some ideas from Twitter, and then just started trying it out and came up with this method. I know there are other ways to do it; maybe even better ways. But this was a simple implementation and has worked great so far. Let me know how it goes for you
Top comments (0)