This is a NestJS template to build a standalone API. It was originally built for a specific purpose (hence the Product Service label and entities) and done as part of an Nx monorepo. This repo specifically is adapted to function on it's own and leaves in much of the original logic so that it could be used as a reference to build something else.
- Docker - We aren't fully leveraging Docker yet. We should be able to spin up the application alongside it's database and ideally want to be able to run all of our tests using these containers with a single command
- "Audit" Tests - One of the E2E tests has a working proof-of-concept to get all routes in the appllication. We should be able to rebuild the E2E tests to check that all routes are versionied and all routes have swagger documentation
- Documentation - The steps in the documentation to recreate a brand new service haven't been tested since splitting off from the monorepo pattern. It was all working before then, but should be checked again
- yarn
- docker
- docker-compose
To get the application up and running immediately, run the following commands from the root directory.
yarn install
docker compose --env-file .env.local up -d
yarn typeorm:cli migration:run
yarn start
This will create the docker container for the database if it doesn't exist, create the schema for product services, and run the server.
The project is setup to use a Postgres container by default. To install and start the container, from the root directory run
docker compose --env-file .env up -d
This will start up a containerized Postgres instance with the values found in .env
. To use an existing local instance of Postgres, simply update the application's .env
to match your instance
All of our database setup is done via the TypeORM CLI. The package.json
includes a command that allows easy access to it:
yarn typeorm <args>
This alias starts by executing identify-entities.ts
which looks for any entity files and updates src/database/entities.ts
which will be imported by the data source. Then it runs the CLI through ts-node so we can natively read .ts
files and is also preconfigured to use the datasource in src/database/data-source.ts
. Using the command without any arguments will show what commands are available.
Once your database is running and the schema has been created, we can setup our entitities using this command
yarn typeorm migration:run
This will apply all migrations from src/database/migrations
to your database
yarn identify-entities
- Searches for new Entity files and updates the module referenced by our data source.yarn typeorm migration:revert
- Undo all applied migrations. This is the best way to reset your databaseyarn typeorm migration:generate <path>
- Create a new migration to sync the database the next time migrations are run. Necessary after any changes are made to Entitiesyarn typeorm migration:create <path>
- Create an empty migration, useful for changes that will not be auto-detected such as DB Functionsyarn typeorm schema:sync
- Sync your database directly against your entities. May miss any manually generated migrationsyarn typeorm schema:drop
- Drops the schema to start fresh. May miss any manually generated migrations
The migration
commands should be used whenever possible. If something goes wrong while changing entities, the schema
can help get your database back into a stable state.
With packages installed and the databse running, you can verify that everything is working correctly by running the test suites
yarn test
- runs all unit testsyarn test:integration
- runs all.integration
tests. Requires a running instance of the DB and will verify that the database is setup correctlyyarn test:all
- runs both unit and integration tests. E2E are excludedyarn test:e2e
- runs only E2E tests. Requires a running instance of the application (yarn start
from another command prompt to test locally)
We have a separate command to quickly enter Jest's watch mode
yarn watch
This will start up watch mode with an invalid file match parameter ("undefined"
) to prevent tests from running automatically. This allows you to easily specify specific files or tests to quickly debug issues.
Before starting everything up, we'll need some test data to get something back from our requests.
yarn seed-data
This uses the same process that our tests use to seed test data. It runs a fake test suite that seeds the database without the normal teardown step. Since it's using the same process, running integration tests will also clear out these records after they finish so you may need to re-seed
Now we can start the application with
yarn start
If everything starts correctly you'll be able to make requests against http://localhost:3000/api/product-services
with the appropriate version header
You can also see the current documentation at: http://localhost:3000/api/product-services/swagger/
To demonstrate how we can leverage the existing Product Services, we'll be using it to build a fun, fictional service to manage Hamsters. This will both show how Product Services is setup, as well as showing how you can use it to build a whole new API.
If you're happy with having the application up and running and ready for dev work, you can stop reading here.
To build our own service based on Products Services, we'll start by copying over the existing product-services
app into a new hamster-services
directory and cleaning out the code that we won't be needing.
- Nest Module - remove the
products
,entitlements
, andresources
folders from thesrc
directory. Insrc/app/app.module.ts
remove references to these deleted imports. - Database - remove migrations in the
src/database/migrations
andsrc/database/test-migrations
directories. Replace the contents ofentities.ts
withexport default []
, this file will be updated automtically but may cause problems if it has broken imports. - Testing - In
src/testing/fixtures
remove fixtures for entitlements, products, and resources and remove the references inindex.ts
. intest/main.e2e-spec.ts
resetversionedRoutes
to{ hamsters: [] }
- App Name - replace all instances of
product-services
withhamster-services
(or your service name) injest.config.js
,seed-test-data.config.js
,project.json
, and the.env
files in the application root directory. Also replaceproduct_services
as the DB schema with an appropriate snake_case value in the.env
files.
To start, we'll be using the Nest CLI to generate the Module
, Controller
, and Service
for our new entity. For demonstration purposes, I will be creating an API to manage adorable Hamsters. These components can be created by running these commands in order
(Note: it is recommended to use a plural name, ie hamsters
instead of hamster
, singular names are typically only used in the Entity and Dto files later on)
nest generate module hamsters
nest generate controller hamsters
nest generate service hamsters
These will automatically create a new folder for your components, and as each command is run it will automatically connect your Controller
and Service
to the new Module
.
The Nest CLI has connected some of these components together, but there's still a few steps we need to do to make sure everything is setup.
We can connect our Service
to the Controller
by importing it into the Controller
and adding this constructor
constructor(private readonly hamstersService: HamstersService) {}
Finally we need to import our Module
into our AppModule
in src/app/app.module.ts
by importing it and adding into the @Module
decorator's import property
imports: [
...
HamstersModule
]
Nestjs comes with a lot of support for TypeORM which is what we'll be using. To start, we need to define our schema with an Entity
file. In our hamsters
directory we'll add a new file
// src/hamsters/hamster.entity.ts
import { Expose } from 'class-transformer'
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm'
@Entity({ name: 'hamsters' })
export class Hamster {
@PrimaryGeneratedColumn('identity', { name: 'id', generatedIdentity: 'BY DEFAULT' })
id: number
@Column('varchar')
name: string
@Column('varchar')
description: string
@CreateDateColumn({ type: 'timestamptz' })
@Expose({ name: 'created_at'})
createdAt: Date
}
This will give us a very basic schema with a sequential ID, a little information abour our hamster, and an auto-generated created time so we know our fuzzy friend's birthday. For the decorators, we also have a demonstration on how to define a custom table name in the @Entity()
decorator and how to specify basic column types.
Data transfer object (DTO) objects are used to represent what incoming and outgoing data will look like. You can structure these however you like. For demonstration purposes I will be using a "plain" Dto to represent what the data will look like in an outgoing response, and a Create
and Update
dto to represent an incoming POST
and PATCH
request body. We'll also be using a common ChangeHamsterDto
and exporting it as two separate objects. This allows us to have cleaner, common, code in the short term and make it easier for these objects to diverge in the future if needed. These will be in a new /hamsters/dtos
directory.
// src/hamsters/dtos/hamster.dto.ts
import { ApiProperty } from '@nestjs/swagger'
import { Expose, Transform } from 'class-transformer'
export class HamsterDto {
@ApiProperty({
description: 'Hamster ID',
example: 1
})
@Expose()
id: number
@ApiProperty({
description: `Hamster's name`,
example: `Boo`
})
@Expose()
name: string
@ApiProperty({
description: 'Important details about our fluffy friend',
example: 'Go for the eyes Boo, GO FOR THE EYES!!'
})
@Expose()
description: string
@ApiProperty({
name: 'created_at',
description: 'ISO string when the Hamster was created',
example: '2022-6-05T14:48:00.000Z'
})
@Expose({ name: 'createdAt'})
@Transform(({ value }) => value.toISOString())
created_at: string
}
// src/hamsters/dtos/change-hamster.dto.ts
import { ApiProperty } from '@nestjs/swagger'
import { IsOptional, IsString } from 'class-validator'
class ChangeHamsterDto {
@ApiProperty({
description: `Hamster's name`,
example: `Boo`
})
@IsString()
@IsOptional()
name?: string
@ApiProperty({
description: 'Important details about our fluffy friend',
example: 'Go for the eyes Boo, GO FOR THE EYES!!'
})
@IsString()
@IsOptional()
description?: string
}
export class CreateHamsterDto extends ChangeHamsterDto {}
export class UpdateHamsterDto extends ChangeHamsterDto {}
// src/hamsters/dtos/index.ts
export { HamsterDto } from './hamster.dto'
export { CreateHamsterDto } from './change-hamster.dto'
export { UpdateHamsterDto } from './change-hamster.dto'
A few important details from the examples above:
- The
@Expose()
decorators are needed to handle the transformations back and forth between objects. They are necessary in the outgoingHamsterDto
. In the incoming Dtos they are optional but can set additional flags for transformations. - We're using a mix of snake_case and camelCase. This is by design and let's Nest automatically convert into different naming conventions where appropriate. The
@Expose()
decorator informs what format we expect when converting into this object. Our outgoingHamsterDto
will be created from the camelCase Entity. Our incomingCreateHamsterDto
andUpdateHamsterDto
will be created from a snake_caseHttpRequest
- Input validation is handled by
class-validator
decorators. In this case@IsString()
checks that the value is string. When combined with@IsOptional()
it will allow the field to be omitted, but must pass@IsString()
if it is included. - The
@ApiProperty()
decorator provides swagger details for our objects.description
andexample
should be provided for all fields,name
is only necessary if the property name in code is camelCase
Now that we have our data objects in place, we need sync our database with the schema represented in code. If your database isn't already running, you can find instructions on how to do so earlier in this document.
With the database running, we can create the schema from our Entity
file by running these commands
yarn typeorm migration:generate --name=bootstrap
yarn typeorm migration:run
These will create the schema in our database, create a new migration in src/database/migrations
named bootstrap
, and then apply it to our brand new schema.
We need to make two changes to our Module
and Service
to finish setting up our connection to the database.
// src/hamsters/hamsters.module.ts
...
import { TypeOrmModule } from '@nestjs/typeorm'
import { Hamster } from './hamster.entity'
@Module({
imports: [TypeOrmModule.forFeature([Hamster])],
...
})
...
// src/hamsters/hamsters.service.ts
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { extendRepo, IExtendedRepository } from '../database/extend-repo'
import { Hamster } from './hamster.entity'
@Injectable()
export class HamstersService {
constructor(@InjectRepository(Hamster) private repo: IExtendedRepository<Hamster>) {
this.repo = extendRepo(this.repo, Hamster)
}
}
In the Module
we are telling nest to bring in TypeORM functionality for our Hamster
entity. In the Service
we are asking nest to provided us with a configured Repository
for our entity. The constructor and definition of IExtendedRepository
are custom code that's adding new functions to the default TypeORM Repository
class.
Test data is handled by our Fixture
classes. An example fixture can be found in src/testing/fixtures/example
. We can copy this user.fixture.ts
up one level into the base fixtures
directory and start updaing it to match our Hamster objects.
Quick breakdown on what should go in each variable
data
- A set ofkey: Entity
pairs in plain JSON. These can represent existing data or what an entity will look like after changes are madeentityKeysToSeed
- Which of yourdata
objects should be used to populate the database, in the test case we are only included one object and using the other two only to check expected resultsrequestDtos
- these are Dto objects used as inputs for CRUD operations. These are also keyed for easy access. At the top of the file is an optional custom type to make autocompletion available when writing tests.errors
- a list of errors that will be returned by your controller and service. This helps ensure error messages are kept consistent with the code and all tests.toDto
- this is an optional method that will be attached to yourdata
and should roughly outline the process to convert anEntity
into theHamsterDto
. DO NOT DUPLICATE THS CODE. This process should not be copied to or from elsewhere in the project. Doing so will risk replicating a bug from your actual code in your tests. There is a default implementation that works for many cases, but can be overriden if needed.
When everything is completed you should have something like this
// src/testing/fixtures/hamsters.fixture.ts
import { BaseFixture, DataItem } from './base.fixture'
import { NotFoundException } from '@nestjs/common'
import { Hamster } from '../../hamsters/hamster.entity'
import { HamsterDto, CreateHamsterDto, UpdateHamsterDto } from '../../hamsters/dtos'
type HamsterDtos = {
createHamsterDto: CreateHamsterDto,
updateHamsterDto: UpdateHamsterDto
}
export class HamstersFixture extends BaseFixture<Hamster, HamsterDto> {
constructor() {
super(Hamster)
this.initialize()
}
public entityKeysToSeed: string[] = ['testHamster']
public data: { [key: string]: DataItem<Hamster, HamsterDto>} = {
testHamster: {
id: 1,
name: 'Boo',
description: 'Go for the eyes Boo, GO FOR THE EYES!!',
createdAt: new Date('1/1/2020')
},
createdHamster: {
id: expect.anything(),
name: 'Floof Nugget',
description: 'maximum floof',
createdAt: expect.anything()
},
updatedHamster: {
id: 1,
name: 'Boo',
description: 'We are all heroes: You and Boo and I. Hamsters and rangers everywhere! Rejoice!',
createdAt: new Date('1/1/2020')
}
}
public requestDtos: HamsterDtos = {
createHamsterDto: {
name: 'Floof Nugget',
description: 'maximum floof',
},
updateHamsterDto: {
description: 'We are all heroes: You and Boo and I. Hamsters and rangers everywhere! Rejoice!',
}
}
public errors: { [key: string]: Error } = {
notFound: new NotFoundException('hamster not found')
}
}
This should also be exported in the index.ts
export { HamstersFixture } from './hamsters.fixture'
To write our first tests, we'll start by creating a new test
directory in src/hamsters
. Next we'll move all of the spec.ts
into and make a copy of hamsters.service.spec.ts
that will be renamed to hamsters.service.integration.spec.ts
. Then open the new integration.spec
file to start writing our integration tests.
Since these tests are going to run against our actual database, we need to setup the testing module the same way we setup the actual modulde by adding this into the createTestingModule()
configuration
import { TypeOrmModule } from '@nestjs/typeorm'
import { Hamster } from '../hamster.entity'
import { dbConfig } from '../../database/data-source'
...
imports: [
TypeOrmModule.forRoot(dbConfig),
TypeOrmModule.forFeature([Hamster])
],
Then we'll need to setup hooks to create and clear test data between each of our tests
import { TestUtil } from '../../testing/util'
...
beforeEach(async () => {
await TestUtil.deleteTestDbRecords()
await TestUtil.createTestDbRecords()
})
Next is to make sure our data has been cleared and the connection to the database closed after all of our tests have finished.
afterAll(async () => {
await TestUtil.deleteTestDbRecords()
await TestUtil.teardownTestDb()
await module.close()
})
We also want to bring in our HamstersFixture
. I also like to use object destructuring to setup which data objects I plan to use in my tests.
import { HamstersFixture } from '../../testing/fixtures/'
...
describe('HamstersService', () => {
const hamstersFixture = new HamstersFixture()
const { testHamster } = hamstersFixture.data
...
Finally, the testing module is not in scope to close the connection so we need to expose it at the top of our describe()
block
...
let module: TestingModule
beforeEach(async () => {
module = await Test.createTestingModule({
...
When everything is finished we should have something that looks like this
// src/hamsters/hamsters.service.integration.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { HamstersService } from '../hamsters.service'
import { TypeOrmModule } from '@nestjs/typeorm'
import { Hamster } from '../hamster.entity'
import { dbConfig } from '../../database/data-source'
import { TestUtil } from '../../testing/util'
import { HamstersFixture } from '../../testing/fixtures/'
describe('HamstersService', () => {
const hamstersFixture = new HamstersFixture()
const { testHamster } = hamstersFixture.data
let service: HamstersService;
let module: TestingModule
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot(dbConfig),
TypeOrmModule.forFeature([Hamster])
],
providers: [HamstersService],
}).compile();
service = module.get<HamstersService>(HamstersService);
});
beforeEach(async () => {
await TestUtil.deleteTestDbRecords()
await TestUtil.createTestDbRecords()
})
afterAll(async () => {
await TestUtil.deleteTestDbRecords()
await TestUtil.teardownTestDb()
await module.close()
})
it('should be defined', () => {
expect(service).toBeDefined()
})
})
Since the test that came with doesn't really help us, we're going replace it with a new integration test that will connect to the database.
describe('getById()', () => {
it('should return a Hamster by id', async () => {
await expect(service.getById(testHamster.id))
.resolves.toEqual(testHamster.toEntity())
})
})
And the matching implementation in our Service
async getById(id: number): Promise<Hamster> {
const hamster = id ? await this.repo.findOneBy({ id }) : null
if (!hamster) {
throw new NotFoundException('hamster not found')
}
return hamster
}
Now we can run our test and should see one passing test
yarn test:integration
Note: you can also use Jest's watch mode for this and future tests. Using the file match option and specifying hamsters.service.int
would filter out all other tests except this new integration test suite. Details on it's use are above in the testing section.
Our final test file should look like this
//src/hamsters/tests/hamster.service.integration.spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { HamstersService } from '../hamsters.service'
import { TypeOrmModule } from '@nestjs/typeorm'
import { Hamster } from '../hamster.entity'
import { dbConfig } from '../../database/data-source'
import { TestUtil } from '../../testing/util'
import { HamstersFixture } from '../../testing/fixtures/'
describe('HamstersService', () => {
const hamstersFixture = new HamstersFixture()
const { testHamster } = hamstersFixture.data
let service: HamstersService;
let module: TestingModule
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot(dbConfig),
TypeOrmModule.forFeature([Hamster])
],
providers: [HamstersService],
}).compile()
service = module.get<HamstersService>(HamstersService)
});
beforeEach(async () => {
await TestUtil.deleteTestDbRecords()
await TestUtil.createTestDbRecords()
})
afterAll(async () => {
await TestUtil.deleteTestDbRecords()
await TestUtil.teardownTestDb()
await module.close()
})
describe('getById()', () => {
it('should return a Hamster by id', async () => {
await expect(service.getById(testHamster.id)).resolves.toEqual(testHamster.toEntity())
})
})
})
Integration tests are a great way to verify that the actual architecture you're trying to build is working. These tests can also take a long time to run and you may not want to run them that often. It's also harder to check for errors and edge cases since you have to actually set up your databse.
To address these concerns, we're going to also add some unit tests to the hamsters.service.spec.ts
that we copied before to build our integration tests.
Since the service is expecting a Repository
, we need to provide a Mocked version of the repository to simulate our database with one of our test utilities. This function returns an array of Mock providers that nest will use to supply a repository.
import { TestUtil } from '../../testing/util'
import { Hamster } from '../hamster.entity'
...
providers: [
HamstersService,
...TestUtil.mockRepositories([Hamster])
],
We also need to get a copy of the repository to simulate database operations in our tests. It's also not a bad idea to get our Fixture setup as well.
import { IExtendedRepository } from '../../database/extend-repo'
import { HamstersFixture } from '../../testing/fixtures/'
...
describe('HamstersService', () => {
const hamstersFixture = new HamstersFixture()
const { testHamster } = hamstersFixture.data
let service: HamstersService
let repo: IExtendedRepository<Hamster>
beforeEach(async () => {
...
service = module.get<HamstersService>(HamstersService)
repo = module.get(getRepositoryToken(Hamster))
})
At this point it should look like
// src/hamsters/tests/hamsters.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { getRepositoryToken } from '@nestjs/typeorm'
import { IExtendedRepository } from '../../database/extend-repo'
import { TestUtil } from '../../testing/util'
import { Hamster } from '../hamster.entity'
import { HamstersService } from '../hamsters.service'
import { HamstersFixture } from '../../testing/fixtures/'
describe('HamstersService', () => {
const hamstersFixture = new HamstersFixture()
const { testHamster } = hamstersFixture.data
let service: HamstersService
let repo: IExtendedRepository<Hamster>
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
HamstersService,
...TestUtil.mockRepositories([Hamster])
],
}).compile()
service = module.get<HamstersService>(HamstersService)
repo = module.get(getRepositoryToken(Hamster))
})
it('should be defined', () => {
expect(service).toBeDefined();
})
})
Now we can add in a unit test to verify our service is calling the repository as expected
describe('getById()', () => {
it('should return a Hamster by id', async () => {
jest.spyOn(repo, 'findOneBy').mockResolvedValue(testHamster.toEntity())
await expect(service.getById(testHamster.id)).resolves.toEqual(testHamster.toEntity())
})
})
If we run our unit tests, we should our new test passing along with most of the existing unit tests. We'll address the controller unit tests soon.
yarn test
Most of the Controller
is already setup for us using the nest CLI. Here's what we'll end up with after adding route to use the Service
function we've created
// src/hamsters/hamsters.controller.ts
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common'
import { Serialize } from '../interceptors/serialize.interceptor'
import { HamsterDto } from './dtos'
import { HamstersService } from './hamsters.service'
@Serialize(HamsterDto)
@Controller({ path: 'hamsters', version: '1' })
export class HamstersController {
constructor(private readonly hamstersService: HamstersService) {}
@Get(':id')
getById(@Param('id', ParseIntPipe) id: number) {
console.log(id)
return this.hamstersService.getById(id)
}
}
As you can see, most of the routing is handled using decorators
@Get(':id')
indicates this should be called with aGET
request, and the string we pass in is the endpoint we want to use. This will also be compined with the contents of the@Controller()
decorator. So our final endpoint will be/hamsters/:id
@Param()
tells nest to extract this parameter from our route parameters andParseIntPipe
converts it from the raw string to a number@Serialize()
is a custom interceptor of ours that will automatically convert theHamster
entities returned from the controller intoHamsterDto
for the response body
Controller level tests are an odd case. Since Nest is doing all of the routing behind the scenes we can't actually test that our routes are working with these tests which means they only provide a little bit of confidence in your application.
We'll be including them here just to prove that they're returning the result of the service. The only difference between the Controller
and Services
tests are mocking the Service
rather than the Repository
// src/hamsters/tests/hamsters.controller.spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { HamstersFixture } from '../../testing/fixtures'
import { TestUtil } from '../../testing/util'
import { HamstersController } from '../hamsters.controller'
import { HamstersService } from '../hamsters.service'
describe('HamstersController', () => {
const hamstersFixture = new HamstersFixture()
const { testHamster } = hamstersFixture.data
let controller: HamstersController;
let service: HamstersService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [HamstersController],
providers: TestUtil.mockProviders([HamstersService])
}).compile();
controller = module.get<HamstersController>(HamstersController);
service = module.get<HamstersService>(HamstersService)
});
// GET /products/:id
describe('getById()', () => {
it('should return the result from service', async () => {
jest.spyOn(service, 'getById').mockResolvedValue(testHamster.toEntity())
await expect(controller.getById(testHamster.id)).resolves.toEqual(testHamster.toEntity())
})
})
})
With all of our code in place to power our endpoint, it's time to prove it actually works with an actual web request. In other APIs these would be considered End to End (E2E) tests. With Nest, we're setting a test instance of our AppModule
that we can send requests to using supertest
. Any middleware in src/main.ts
that's being applied to our live server needs to be tested separately, which is why we make this distinction between the Controller
integration tests and true E2E tests.
Our test file will look a lot like the service.integration
tests, except that we are using supertest to run through our code.
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication } from '@nestjs/common'
import request from 'supertest'
import { AppModule } from '../src/app/app.module'
import { HamsterFixture } from '../src/testing/fixtures'
import { TestUtil } from '../src/testing/util'
import { StatusCodes } from 'http-status-codes'
describe('Product Controller', () => {
const hamstersFixture = new HamsterFixture()
const { testHamster } = hamstersFixture.data
const { OK } = StatusCodes
let app: INestApplication
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile()
app = moduleFixture.createNestApplication()
await app.init()
})
beforeEach(async () => {
await TestUtil.deleteTestDbRecords()
await TestUtil.createTestDbRecords()
})
afterAll(async () => {
await TestUtil.deleteTestDbRecords()
await TestUtil.teardownTestDb()
await app.close()
})
describe('GET /hamsters/:id', () => {
it('should return OK and a Hamster', async () => {
const response = await request(app.getHttpServer())
.get(`/hamsters/${testHamster.id}`)
.expect(OK)
expect(response.body).toEqual(testHamster.toDto())
})
})
})
We should see all of our integration tests passing with
yarn test:integration
Our E2E tests covers all of the logic in src/main.ts
which includes things like versioning, swagger documentation, and datadog integration. Many of the tests for product services will also work here with a few small changes.
In main.ts
we need to set a new globalPrefix
const globalPrefix = '/api/hamster-services'
Then in test/main.e2e-spec.ts
we need to update the global routePrefix
to match
const routePrefix = '/api/hamster-services'
And configure our versionedRoutes
to check that our first endpoint is looking for a version header
const versionedRoutes = {
hamsters: [
new VersionedRoute('GET', '/hamsters/-1', '1'),
]
}
Running E2E tests is a little different than our other tests since we aren't using Nest's testing modules. Instead in a new command prompt start the server with
yarn start
And in our original command prompt
yarn test:e2e
We should get back two passing tests
If you've been following along you should now have a fully functioning and fully tested application (albeit a very basic one). From here you can go back and implement/test the remaining endpoints and CRUD operations for whichever Entity you chose to start with.
And while we have tests to prove our one and only endpoint is working all the way from the browser down to the database, we've only tested our "ideal" scenario. Now would be a good time to go back and add some additional tests for what happens when we can't find our beloved Hamster.