A simple modular RESTful microservice built with Express, MongoDB, and the core-ts ecosystem.
This project demonstrates how to build clean, lightweight, and maintainable microservices without relying on heavy frameworks or decorators. It follows a layered architecture with clear separation between controllers, use cases, repositories, and models.
Unlike the more opinionated samples, this project keeps the request-processing flow explicit, making it an excellent starting point for learning how the Core TS libraries work together.
- Modular project structure
- RESTful CRUD APIs
- Configuration management
- Structured logging
- Health check endpoint
- Localization support
- Request validation
- Generic CRUD services
- Generic MongoDB repository
- MongoDB support
- Express 5 compatible
Runs the app in the development mode.
Builds the app for production to the dist folder.
Runs the app for production in the dist folder.
docker build -t sql-simple-modular-sample .docker run -p 8080:8080 sql-simple-modular-samplegcloud run deploy mongo-simple-service \
--image us-central1-docker.pkg.dev/<your-project-id>/node-repo/mongo-simple-modular-sample \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--port 8080 \Business logic is independent of the MongoDB driver. Repositories depend on the generic DB abstraction provided by sql-core.
HTTP Request
│
▼
Express Controller
│
▼
Service Layer
│
▼
Repository Layer
│
▼
mongodb-kit
│
▼
MongoDB
- TypeScript
- Node.js
- Express 5
- MongoDB
Core TS libraries:
- config-plus
- logger-core
- middleware-logging
- express-web-kit
- types-validation
- validation-core
- mongodb-kit
- onecore
src
│
├── app.ts # Application bootstrap
├── config.ts # Application configuration
├── context.ts # Dependency composition
├── route.ts # Route registration
│
├── resources/ # Localization resources
│
└── user/
├── user.ts
├── repository.ts
├── service.ts
├── controller.ts
└── index.ts
Each business feature is organized into its own module, making the project easy to extend and maintain.
Controllers are responsible for:
- Receiving HTTP requests
- Parsing request parameters
- Validating input
- Calling business services
- Returning HTTP responses
Business logic remains inside the use case layer.
The service layer contains application business logic.
Most CRUD functionality is inherited from reusable generic implementations provided by onecore, allowing services to stay small and focused.
export class UserUseCase
extends UseCase<User, string, UserFilter> implements UserServiceResponsible for database access.
export class MongoUserRepository
extends Repository<User, string, UserFilter> implements UserRepository {
constructor(db: Db) {
super(db, "users", userModel)
}
}Each repository extends reusable Repository implementations provided by mongodb-kit, requiring only:
- Mongo Database
- Collection name
- Entity model
Everything else is inherited from mongodb-kit.
Custom queries can easily be added when needed.
This greatly reduces boilerplate while keeping the repository extensible for custom queries.
Models define:
- Entity structure
- Validation rules
- Metadata
Validation rules are centralized using validation-core, keeping controllers concise and consistent.
Dependencies are wired explicitly rather than using a dependency injection framework.
MongoDB
│
▼
Repository
│
▼
Service
│
▼
Controller
All dependencies are created in context.ts, serving as the application's composition root.
Unlike more abstract samples, this project keeps the HTTP flow explicit.
HTTP Request
│
▼
Controller
│
▼
Parse Request
│
▼
Validate Model
│
▼
Service
│
▼
Repository
│
▼
MongoDB
│
▼
HTTP Response
This makes the project particularly useful for developers learning the Core TS ecosystem.
Validation and application messages support multiple languages.
const resource = getResource(req)The project supports:
- English
- Vietnamese
Localization resources are located under:
src/resources/
Validation messages automatically use the selected language.
This is a nice feature that many samples omit.
New languages can be added without changing business logic.
Validation is handled by validation-core, with rules defined in the model.
const resource = getResource(req)
const errors = validate<User>(user, userModel, resource)Validation rules are defined alongside the entity model rather than inside controllers.
Examples include:
- Required fields
- Email validation
- Length constraints
- Pattern validation
- Primary key validation
Type validation is also integrated into routing through:
const checkUser = check(userModel)and reinforced in controller methods. This shows both middleware-based and controller-level validation approaches.
The application uses MiddlewareLogger from middleware-logging together with logger-core for structured request logging. This provides a consistent logging strategy across the application.
The sample uses structured request logging through middleware-logging.
Middleware logging is configurable:
- HTTP method
- Request URL
- Request
- Response
- Status code
- Response size
- Execution time
It also propagates:
- Request ID
- Correlation ID
which is useful in distributed systems.
Sensitive request and response data can be masked before logging.
The sample includes a health endpoint suitable for containerized deployments.
The endpoint checks:
- MongoDB connectivity
They can easily be extended to include additional infrastructure services.
Example response
{
"status": "UP",
"details": {
"mongodb": {
"status": "UP"
}
}
}Configuration is managed using config-plus, merging:
Default Configuration
│
▼
Environment Configuration (SIT, UAT, PRD)
│
▼
Environment Variables (process.env)
│
▼
Final Configuration
Typical configuration includes:
- HTTP server
- MongoDB connection
- Logging
- Localization
- Application settings
Environment variables can override default values for different deployment environments.
Each business module follows the same structure.
customer/
controller.ts
service.ts
repository.ts
customer.ts
index.ts
To add a new module:
- Define the entity model.
- Create the repository.
- Create the use case.
- Create the controller.
- Register routes.
- Add the controller to the application context.
This consistent approach keeps the application modular and easy to maintain.
This project is designed to be the simplest SQL sample in the Core TS ecosystem.
It demonstrates:
- Explicit request processing
- Layered architecture
- Structured logging
- Request validation
- Reusable CRUD services
- Generic repositories
- MongoDB integration
without introducing unnecessary complexity.
It is an excellent starting point for developers who want to understand how the ecosystem works before adopting more advanced abstractions.
This sample demonstrates how several core-ts libraries work together.
| Library | Purpose |
|---|---|
express-web-kit |
Express utilities and REST helpers |
middleware-logging |
HTTP request and response logging |
validation-core |
High-performance validation library |
onecore |
Generic CRUD use cases and common abstractions |
mongodb-kit |
Generic MongoDB repositories |
logger-core |
Structured logging |
config-plus |
Configuration management |
Each library focuses on a single responsibility.
That demonstrates the intended layering very well.
- sql-modular-sample — SQL modular microservice using MySQL
- sql-simple-modular-sample — SQL modular microservice using PosgreSQL
- mongo-simple-modular-sample — MongoDB modular microservice
These samples share the same architecture, allowing developers to switch databases while keeping the application structure consistent.
- Simple
- Lightweight
- Modular
- Database-independent
- Enterprise-friendly
- Production-ready
- Explicit
- Testable
The project stands out for several reasons:
- Very explicit flow from HTTP request to database.
- Clear layered architecture with well-defined responsibilities.
- Minimal boilerplate thanks to generic repositories and use cases.
- Database abstraction through
mongodb-kit. - Good observability with structured logging and health checks.
- Simple dependency composition without a DI container.
To check if the service is available
{
"status": "UP",
"details": {
"mongodb": {
"status": "UP"
}
}
}In the below sample, search users with these criteria:
- get users of page "1", with page size "20"
- email="tony": get users with email starting with "tony"
- dateOfBirth between "min" and "max" (between 1953-11-16 and 1976-11-16)
- sort by phone ascending, id descending
{
"page": 1,
"limit": 20,
"sort": "phone,-id",
"email": "tony",
"dateOfBirth": {
"min": "1953-11-16T00:00:00+07:00",
"max": "1976-11-16T00:00:00+07:00"
}
}GET /users/search?page=1&limit=2&email=tony&dateOfBirth.min=1953-11-16T00:00:00+07:00&dateOfBirth.max=1976-11-16T00:00:00+07:00&sort=phone,-id
In this sample, search users with these criteria:
- get users of page "1", with page size "20"
- email="tony": get users with email starting with "tony"
- dateOfBirth between "min" and "max" (between 1953-11-16 and 1976-11-16)
- sort by phone ascending, id descending
- total: total of users, which is used to calculate numbers of pages at client
- list: list of users
{
"list": [
{
"id": "ironman",
"username": "tony.stark",
"email": "tony.stark@gmail.com",
"phone": "0987654321",
"dateOfBirth": "1963-03-24T17:00:00Z"
}
],
"total": 1
}[
{
"id": "spiderman",
"username": "peter.parker",
"email": "peter.parker@gmail.com",
"phone": "0987654321",
"dateOfBirth": "1962-08-25T16:59:59.999Z"
},
{
"id": "wolverine",
"username": "james.howlett",
"email": "james.howlett@gmail.com",
"phone": "0987654321",
"dateOfBirth": "1974-11-16T16:59:59.999Z"
}
]GET /users/wolverine{
"id": "wolverine",
"username": "james.howlett",
"email": "james.howlett@gmail.com",
"phone": "0987654321",
"dateOfBirth": "1974-11-16T16:59:59.999Z"
}{
"id": "wolverine",
"username": "james.howlett",
"email": "james.howlett@gmail.com",
"phone": "0987654321",
"dateOfBirth": "1974-11-16T16:59:59.999Z"
}- status: configurable; 1: success, 0: duplicate key, 4: error
{
"status": 1,
"value": {
"id": "wolverine",
"username": "james.howlett",
"email": "james.howlett@gmail.com",
"phone": "0987654321",
"dateOfBirth": "1974-11-16T00:00:00+07:00"
}
}- Request:
{
"id": "wolverine",
"username": "james.howlett",
"email": "james.howlett",
"phone": "0987654321a",
"dateOfBirth": "1974-11-16T16:59:59.999Z"
}- Response: in this below sample, email and phone are not valid
{
"status": 4,
"errors": [
{
"field": "email",
"code": "email"
},
{
"field": "phone",
"code": "phone"
}
]
}PUT /users/wolverine{
"username": "james.howlett",
"email": "james.howlett@gmail.com",
"phone": "0987654321",
"dateOfBirth": "1974-11-16T16:59:59.999Z"
}- status: configurable; 1: success, 0: duplicate key, 2: version error, 4: error
{
"status": 1,
"value": {
"id": "wolverine",
"username": "james.howlett",
"email": "james.howlett@gmail.com",
"phone": "0987654321",
"dateOfBirth": "1974-11-16T00:00:00+07:00"
}
}Perform a partial update of user. For example, if you want to update 2 fields: email and phone, you can send the request body of below.
PATCH /users/wolverine{
"email": "james.howlett@gmail.com",
"phone": "0987654321"
}- status: configurable; 1: success, 0: duplicate key, 2: version error, 4: error
{
"status": 1,
"value": {
"email": "james.howlett@gmail.com",
"phone": "0987654321"
}
}DELETE /users/wolverine1Runs the app in the development mode.
Builds the app for production to the dist folder.
Runs the app for production in the dist folder.
MIT
