Project Framework

Project Framework

This page explains how you could structure an application introducing a framework that you can optionally apply to a large project that uses Next.js.

The framework combines back-end and front-end code into a single code base. The logical parts of the app are split into folders called "modules" given them corresponding name such as user, post, comment, app settings, auth features etc. Basically, a "module" can belong to 2 categories:

  1. An entity (a model) like "user" (or "post", "comment" etc).
  2. Anything what doesn't belong to some specific entity: app settings, auth, AI stuff...

The typical structure of files and folders in a Vovk.ts app would look like that:

        • HelloState.ts
        • HelloService.ts
        • HelloIsomorphicService.ts
        • HelloWorker.ts
        • HelloState.ts
        • AppState.ts
        • AppState.ts
        • AuthState.ts
        • AuthService.ts
        • AuthController.ts
        • UserState.ts
        • UserService.ts
        • UserController.ts
        • UserIsomorphicService.ts
        • PostState.ts
        • PostService.ts
        • PostIsomorphicService.ts
        • PostController.ts
        • CommentState.ts
        • CommentService.ts
        • CommentController.ts
  • Every item in a module folder (Service Class, Controller Class, state etc) is optional. Some parts of your app would require to have state only, but no controller. In other case you can have a state and a controller, but database request in your controller is too simple to create a separate service class...

    The image below illustrates how different components of the application can be related to each other.

    Controller Class

    Controller Class is a static class that defines API endpoints. It can use Back-End Service Classes and Isomorphic Service Classes explained below.

    /src/modules/post/PostController.ts
    import { prefix, get } from 'vovk';
    import PostService from './PostService';
     
    @prefix('posts')
    export default class PostController {    
        @get()
        static getPosts() {
            return PostService.getPosts();
        }
    }

    Back-end Service Class

    Back-end Service Class (or just "Service") is a static class that implements third-party API calls or performs requests do the project database. By design Services don't have access to the request object and play the role of a "back-end library".

    /src/modules/post/PostService.ts
    export default class PostService {
        static getPosts() {
            return prisma.post.findMany();
        }
        
        static getPostById(postId: Post['id']) {
            return prisma.post.findUniqueOrThrow({ where: { id: postId } });
        }
    }

    Isomorphic Service Class

    Isomorphic Service is similar to a Back-end Service but can be used both by front-end (state, components, hooks, other Isomorphic Services, ...) and back-end (Back-End Services, Controllers, CLI scripts, ...). The only difference is that its methods need to be implemented as pure functions (opens in a new tab). It means that it shouldn't perform DB calls nor access application state but can use other Isomorphic Service Classes.

    /src/modules/comment/CommentIsomorphicService.ts
    import PostIsomorphicService from '../post/PostIsomorphicService';
     
    export default class CommentIsomorphicService {
        // a pure function
        static filterCommentsByPostId(comments: Comment[], posts: Post[], postId: Post['id']) {
            // filterPostById is also a pure function
            const post = PostIsomorphicService.filterPostById(posts, postId);
            if(post.isDeleted) return [];
            return comments.filter((comment) => comment.postId === postId);
        }
     
        // ...
    }

    WPC Class

    Every Isomorphic Service Class can be turned into a WPC Class (Worker Procedure Call) by applying @worker() decorator. The decorator defines required onmessage listeners if it's used in a Web Worker scope. In other cases @worker() decorator does nothing and the class can still be used as an Isomorphic Service somewhere else.

    /src/modules/hello/HelloWorker.ts
    import { worker } from 'vovk';
     
    @worker()
    export default class HelloWorker {
        static performHeavyCalculations() {
            // ...
        }
    }

    WPC Clases can use other WPC Classes, Isomorphic Service Classes and Back-End Controllers imported from vovk-client. For more info check the documentation.

    State

    State file contains application state code that is going to be imported by React Components and other state files. It can use Isomorphic Services, WPC interfaces and clientized Controllers imported from vovk-client. State can be implemented with any application state library, since the framework does not cover state management topic.

    /src/modules/post/PostState.ts
    import { PostController, PostWorker } from 'vovk-client';
     
    // ... init app state for posts

    Other ideas

    The framework isn't limited by the elements described above and you may want to add more files into your module folder.

    • More Back-end Services in order to organize the code further.
    • More Isomorphic Services.
    • More WPC classes.
    • Tests.
    • React Components that you want to categorise (modules/hello/components/MyComponent.tsx).
    • Types (modules/hello/HelloTypes.ts).
    • Anything else you can imagine.

    The framework is a suggestion and you can adjust it to your needs. It's not a strict rule but a way to make your project more structured and maintainable based on the experience of the Vovk.ts creator.