Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use node native stuff #262

Draft
wants to merge 12 commits into
base: dev-containers
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 3 additions & 71 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,72 +1,4 @@
# Environment
/docker/.env
.env

# Compiled output
dist
dist/*
!/dist/v4
!/dist/v5
!/dist/v6

/tmp
/temp
/out-tsc

# Dependencies
/node_modules

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# OS
.DS_Store

# IDEs and editors
/.idea
.project
.classpath
.c9/
node_modules
.idea
.vscode
*.launch
.settings/
*.sublime-workspace

# IDE - VSCode
*.code-workspace
/.vscode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

# Miscellaneous
/.sass-cache
/connect.lock
/libpeerconnection.log
npm-debug.log
testem.log
/typings

# Tests
/coverage
/.nyc_output

# e2e
/e2e/*.js
/e2e/*.map

# System Files
Thumbs.db

# Mock API
/api/node_modules

# Compiled templates
src/app/homepage/pages/**/*.html
.DS_Store
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
20.15.1
22.7.0
18 changes: 0 additions & 18 deletions .swcrc

This file was deleted.

25 changes: 0 additions & 25 deletions Dockerfile

This file was deleted.

217 changes: 4 additions & 213 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,218 +1,9 @@
# Codetalk
# Hono example

![Docker Image](https://img.shields.io/badge/docker-latest-blue)
![Github Workflow](https://github.com/acidtango/boilerplate-nestjs/actions/workflows/main.yaml/badge.svg)
Node 22.6.0 required!

Codetalk is an example application that implements Domain-Driven Design (DDD) and Hexagonal Architecture.
It provides the ability to create talks, events, and speakers, and enables speakers to propose talks that can be reviewed
by event organizers and approved or rejected.

## 💻 I'm a dev, how do I get started?

### Run using [dev containers](https://containers.dev/)
To run it

```
git clone [email protected]:acidtango/boilerplate-nestjs.git
```

Open the devcontainer using your favourite IDE and you are ready to go!!

### Run in local environment

Prerequisites:

- [Node.js](https://nodejs.org/es/download)
- [TypeScript](https://www.typescriptlang.org)
- [Yarn](https://yarnpkg.com/)
- [Docker](https://docs.docker.com/get-docker/)

Now:

```bash
git clone [email protected]:acidtango/boilerplate-nestjs.git
cd codetalk
yarn install
docker compose up -d db aws # starts DDBB & queue
npm start
```

You are now good ready to go!! 👯

### `yarn` scripts

- `build`: Compiles the project for later using `yarn start`
- `start`: Opens the server by compiling the sources on the fly
- `start:dev`: Opens the server compiling the project on the fly in watch mode
- `start:prod`: Opens the server in production mode using the compiled sources
- `typecheck`: Checks the typing integrity of the project
- `lint:check`: Lints all the files
- `lint:fix`: Lints and fixes all the files

- `test`: Runs all the tests
- `test:unitary`: Runs unitary tests
- `test:integration`: Runs integration tests that uses local elements (local database, local event bus, etc...)
- `test:integration:third-party`: Runs integration tests that uses third party elements (stripe, email services, etc...)
- `test:e2e:memory`: Runs E2E tests using the in-memory repositories
- `test:e2e:db`: Runs E2E tests using the real database repositories
- `precommit`: Runs all the necessary commands that would make the CI pass

### Docker

We use Docker as a utility tool, mainly for running MongoDB and LocalStack. In the `docker-compose.yml` you have two services:

- `api`: The API if you want to open it as a docker container
- `db`: A mongodb database that we use for starting the API in development mode and running the integration tests locally.
- `aws`: This is LocalStack, which is an environment simulation of AWS. We use it for the SQS service.

### Project management

- [Trello board](https://example.org/)
- [Github repo](https://github.com/acidtango/boilerplate-nestjs)
- [Github Actions](https://github.com/acidtango/boilerplate-nestjs/actions)
- [Figma](https://example.org/)
- [Firebase console](https://example.org/)

## 🛠 Which technologies are you using?

- [Node](https://nodejs.org/en)
- [Nestjs](https://nestjs.com/)
- Validations with [Class Validator & Class Transformer](https://docs.nestjs.com/techniques/validation)
- [OpenAPI docs](https://docs.nestjs.com/openapi/introduction)
- Mainly used as dependency injection container
- [TypeScript](https://github.com/AgileCraftsmanshipCanarias/kata-setup-typescript)

## 🏘 How is the code organized?

The architecture follows the principles from Hexagonal Architecture, and the final implementation is inspired by [this](https://github.com/CodelyTV/php-ddd-example) and [this](https://github.com/CodelyTV/typescript-ddd-skeleton) repositories from [CodelyTV](https://codely.tv/).

All the main code of the application lives under `src`

### `src`

Under this directory lives all the main application. This root directory contains all the modules of the app, and inside of each module you can find the classic division `domain/use-cases/infrastructure`.

- **Domain**: All the classes needed for modeling the business.
- **Use Cases** (AKA Application): These are specific use cases which orchestrates several domain elements to perform its job.
- **Infrastructure**: All the elements that are coupled to a certain Database/Library/Framework.

For example:

```
.
├── MainModule.ts
├── Token.ts
└── talks
├── TalksModule.ts
├── domain
│ ├── errors
│ │ ├── MaximumCospeakersReachedError.ts
│ │ └── TalkTitleTooLongError.ts
│ ├── Talk.ts
│ └── TalkStatus.ts
├── use-cases
│ ├── ApproveTalk.ts
│ ├── ProposeTalk.ts
│ └── GetTalk.ts
│ └── controllers
└── infrastructure
├── ApproveTalkEndpoint.ts
├── ProposeTalkEndpoint.ts
├── dtos
│ ├── ProposeTalkRequestDTO.ts
│ └── TalkResponseDTO.ts
├── GetTalkEndpoint.ts
└── ReviewTalkEndpoint.ts
```

### 🕴 Dependency Injection, Dependency Inversion

Instead of depending on a certain implementation, we depend on an abstraction (an interface). This allows us to create a more decoupled architecture and facilitates testing.

It's the **D** from the [SOLID](https://en.wikipedia.org/wiki/SOLID) principles.

You can read more about dependency inversion [here](https://en.wikipedia.org/wiki/Dependency_inversion_principle).

- Do not import third-parties or side effect methods into the domain/use cases layer
- Instead, create an interface that represent that interaction
- Create two implementations of that interface:
- A "real" implementation (calling TypeORM, Stripe, Fetch HTTP API Call...).
- A "fake" implementation just for testing purposes.

### Dependency injection container

For wiring up all the dependencies, we are using the native Nestjs dependency container. This is the only thing that we are coupled to, specially from the application layer.

A special thing that we have to take into account, is when injecting interfaces.

The interfaces are a compile-time thing of Typescript, so when we need to inject a certain implementation we need to specify an identifier for that interface with a token.

```typescript
// TalkRepository.interface.ts
export interface TalkRepository {
save(talk: Talk): Promise<void>
findBy(talkId: TalkId): Promise<Talk | undefined>
}
```

```typescript
// Token.ts
export enum Token {
TALK_REPOSITORY = 'TALK_REPOSITORY',
// ...
}
```

Then we need to specify the dependency in the class consuming this interface

```typescript
class PurposeTalk {
constructor(@Inject(Token.TALK_REPOSITORY) private readonly talkRepository: TalkRepository) {}
}
```

Later on, we need to wire up these dependencies from a Nestjs module:

```typescript
// TalkRepositoryModule.ts

@Global()
@Module({
providers: [{ provide: Token.TALK_REPOSITORY, useClass: TalkRepositoryMongo }],
exports: [Token.TALK_REPOSITORY],
})
export class TalkRepositoryModule {}
```

### Inheritance for specification

We are using Inheritance for specifying which element of Hexagonal Architecture is each class.

For example, we are extending a `ValueObject` class when having a value object or a `UseCase` for the use cases.

In general, this inheritance does not have any logic. It's just like a explanatory variable.

Examples:

```typescript
class ReservationLister extends UseCase {
/* ... */
}
class Reservation extends AggregateRoot {
/* ... */
}
class ReservationTitle extends ValueObject<string> {
/* ... */
}
```

## ✅ Tests

- We are using [Jest](https://jestjs.io/) and [tepper](https://github.com/DanielRamosAcosta/tepper) for acceptance tests.
- Unitary Tests are paired with the element that tests. For example `Talk.spec.ts` is next to `Talk.ts`.
- E2E tests lives under the `tests` directory. These tests crosses the framework layers and we interact with the API as a black box. These are the main tests that we use for TDD by performing outside-in. We write then in an acceptance test fashion.

### CI/CD

- The CI and CD are in Github Actions
- We run the precommit script before each commit using Husky.
- The CI runs for both acceptance/unitary and integration tests with a real database.
- After all tests passed, then the API is re-deployed
33 changes: 33 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import globals from 'globals'
import pluginJs from '@eslint/js'
import tseslint from 'typescript-eslint'

const baseConfig = [
{ files: ['**/*.{js,mjs,cjs,ts}'] },
{ languageOptions: { globals: globals.node } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
]

export default [
...baseConfig.map(ruleErrorToWarn),
// custom config
{
rules: {
'@typescript-eslint/parameter-properties': 'error',
},
},
]

function ruleErrorToWarn(rule) {
if ('rules' in rule) {
return {
...rule,
rules: Object.fromEntries(
Object.entries(rule.rules).map(([key, value]) => [key, value === 'error' ? 'warn' : value])
),
}
}

return rule
}
9 changes: 0 additions & 9 deletions eslint.config.mjs

This file was deleted.

3 changes: 0 additions & 3 deletions init-aws.sh

This file was deleted.

Loading