This project serves as a foundational template for our internal design pattern for SvelteKit, which closely resembles the Model-View-Controller (MVC) architecture. Our template is tailored to organize and scale SvelteKit projects effectively, especially when integrating with various external services or databases. This version only demonstrates the read data flow, we would like to include all CRUD operations with examples from APIs in the next.
While initially, the implementation might seem extensive, it is designed to simplify and streamline the process as the project scales. This template currently integrates with a DummyJson product endpoint to demonstrate the basic concepts.
Our design pattern is grounded in the following core principles:
Data Fetching: Only the server is responsible for fetching data, whether from a database or an API. This part of the architecture is known as the Model or Consumer, as it consumes services.
Endpoints: Endpoints in the project establish a connection between the server and the client, functioning as Controllers. Their primary role is to facilitate requests from the server to the consumer and relay responses back without performing any business logic.
View Management: Views are handled through +page.svelte
files. These views receive data exclusively from +page.server.ts
, which, in turn, only requests data from an endpoint.
Request and Response Models: We use TypeScript classes to define the structure of requests to a consumer (Request models
) and the structure of responses received (Response models
). Every response from a consumer is encapsulated in a base response class to ensure consistent data shapes from any service.
Data Transfer Objects (DTOs): DTOs are TypeScript classes that facilitate data transfer from the server (Model) to the UI (View). This encapsulation is particularly useful when your SvelteKit project is a part of a larger application.
Component Guidelines: Components, representing Views, are restricted to rendering data and emitting events. They should not contain any business logic.
Route Structure: Routes should contain only three files: +page.svelte
, +layout.svelte
, and +page.server.ts
. The +page.server.ts
file is divided into three segments: the load
function, actions
function, and a page helper
class. The load
function generates predefined DTOs for the page, while the page helper
class, acting as a local controller for the route, manages data requests and transformations.
One significant advantage of this design pattern is its facilitation of simultaneous development by multiple team members. With a decoupled UI, team members can work independently on different aspects of the project, such as service integration, middleware, and UI development.
To start using this template for your SvelteKit projects:
The lib/server
directory houses server-related integrations, and within it, the httpConsumers
directory plays a crucial role in managing API integrations.
In this template example, the httpConsumers
directory is organized by domain. Each domain has its own httpConsumer
, serving as the integration point with an API where requests are initiated and defined responses are received.
Within each domain's httpConsumer
, you'll find a models
directory. This is where request and response models for that specific domain are defined. As each domain is unique, the request and response models will differ accordingly.
The response
directory, organized by domain, contains definitions for the schemas returned by the integrated API. For instance, in our template example, the path /httpConsumers/dummyJson/models/responses
showcases specific domains like /products
or /users
, where the schema for API responses is defined.
Each directory within httpConsumers
includes an index.ts
file. This file serves the purpose of exporting the contents of the directory, allowing for the creation of namespaces. While this is an optional discipline, it proves beneficial as the project expands in size and complexity.
Instead of importing modules from specific directories individually, such as:
import type { BaseCollectionResponse } from '$lib/server/httpConsumers/rickAndMorty/models/baseCollectionResponse';
import type { BaseResponse } from '$lib/server/httpConsumers/baseResponse';
import type { CharacterResponse } from '$lib/server/httpConsumers/rickAndMorty/models/characters/characterResponse';
import type { ProductCollectionResponse } from '$lib/server/httpConsumers/dummyJson/models/products/productCollectionResponse';
You can streamline imports using a single namespace:
import type {
BaseCollectionResponse,
BaseResponse,
CharacterResponse,
ProductCollectionResponse
} from '$lib/server/httpConsumers';
This approach enhances code organization, especially as the project scales. It provides a cleaner and more manageable structure for importing modules, ultimately contributing to a more maintainable codebase.
The repositories
directory within the lib/server
structure is dedicated to database integrations. This section outlines how to structure and utilize the repositories
directory for effective database management.
In the repositories
directory, we recommend organizing by domain. Each domain represents a distinct section of your application that interacts with a specific database. This organizational approach facilitates a clear separation of concerns and makes it easier to manage database connections and entities.
Within each domain, you should define the database connection. This is the entry point for interacting with the database associated with that domain. The connection configuration, credentials, and any other pertinent details should be specified here.
Alongside the database connection, the entities
directory within each domain contains definitions for entities or models that represent the structure of your database tables. These entities define the fields, relationships, and constraints within the database.
Here's an example directory structure for the repositories
directory:
/lib
/server
/repositories
/users
- index.ts (Exports contents of the users directory)
- databaseConnection.ts (Database connection configuration)
/entities
- UserEntity.ts (Definition for the User entity/model)
/products
- index.ts
- databaseConnection.ts
/entities
- ProductEntity.ts
Within lib/shared
, you can expect to find functionality that is universally applicable to both the client and server aspects of your project. This includes modules, dtos, utilities, and other shared resources that contribute to the overall cohesion of your application.
In the context of our MVC architecture, Data Transfer Objects (DTOs) manage the flow of data between different components of the application. DTOs act as structured containers for data, defining the format and shape of information exchanged between the server (Models) and the UI (+page.svelte).
Encapsulation and Structure: DTOs encapsulate data, providing a structured and well-defined format. They serve as containers for data that needs to be transferred between different layers of the application.
Decoupling Layers: By using DTOs, we achieve a level of decoupling between the server and the UI. The server can package data into DTOs, and the UI can expect a consistent data structure, regardless of the complexities within the server.
Consistent Data Shape: All responses from any consumer (Model) are wrapped in a base response class. DTOs contribute to this consistency by defining the expected structure of the data, ensuring uniformity across various services.
DTOs are organized by domain, aligning with the structure of the application. Each domain has its own set of DTOs, reflecting the specific data requirements for that domain. This organization promotes clarity and ensures that data transfer is contextually relevant.
In addition to domain-specific DTOs, the views
directory allows you to define composite DTOs. These composite DTOs aggregate data from multiple sources to create a unified structure for the UI (+page.svelte). This proves particularly useful when your SvelteKit project is part of a larger application with diverse data sources.
In your routes (+page.server.ts
files), you can utilize DTOs to structure and package data before sending it to the UI. For example:
// +page.server.ts
import type { PageServerLoad } from './$types';
import type { BaseResponse, ProductCollectionResponse } from '$lib/server/httpConsumers';
import { ProductCollectionDto } from '$lib/shared/dtos/products';
import { BasePageDataDto, SeoDataDto } from '$lib/shared/dtos';
export const load: PageServerLoad = async ({ fetch, url }) => {
const dummyJsonProductsPromise: Promise<ProductCollectionResponse> =
PageHelper.getDummyJsonProducts(fetch, url.searchParams);
const [dummyJsonProducts] = await Promise.all([dummyJsonProductsPromise]);
const seo = PageHelper.getSeoMetaData(url);
const pageData = new ProductCollectionDto(dummyJsonProducts);
const pageMeta = new SeoDataDto(seo);
return {
...BasePageDataDto.getSuccessResponse<ProductCollectionDto, SeoDataDto>(
pageData,
pageMeta,
null,
null
)
};
};
DTOs enhance the clarity, maintainability, and consistency of your application's data flow, contributing to a robust MVC architecture.
This template is an integral part of our development process at Kreonovo.com, ensuring that our SvelteKit projects are scalable, maintainable, and efficiently structured. We hope it serves you well in your development endeavors.
Feel free to adapt this template to your specific needs and project details. This README provides a basic overview of your design pattern and how it integrates within a SvelteKit project.