Code Monkey home page Code Monkey logo

bistrio's Introduction

Japanese Version

What is this?

This is a web application framework made with TypeScript, React, and Zod. It focuses on the presentation layer as described in the three-tier architecture, covering both the frontend written in JSX and the backend layer (the surface of the server) that communicates with the frontend.

Challenges and Solutions

Many frameworks like Next.js primarily focus on the frontend. In such cases, I believe there are several challenges:

  • Often, non-essential tasks related to connecting with the backend (such as adjusting URLs for communication) arise.
  • Using Server Side Rendering (SSR) necessitates a frontend server (BFF), complicating the system architecture.

To address these challenges, our framework takes responsibility "from the frontend to the entrance of the backend." This approach results in:

  • Automatic generation of frontend communication code from server code, enabling communication through method calls on objects, thus eliminating the need for manual communication setup.
  • No need for a BFF, as the framework includes backend components.

The backend is built following the philosophy of REST, defining Resources and their methods as the framework's responsibility. Since it's not a full-stack solution, you can choose any database or other components for implementing Resources.

Features

  • Designed to minimize differences between content rendered on the client and the server for SSR.
  • Pages written in JSX can call Resource objects exposed by the server, similar to making Remote Procedure Calls (RPC) to the types of objects exposed by the server.
    • At runtime, this works as follows:
      • CSR: Calls the API through stubs of automatically generated Resource objects.
      • SSR: Directly calls Resource objects, performing database connections within the same process for efficient execution.
  • Writing code for Routes and Resources automatically generates a lot of code, saving developers from writing repetitive, boring code. This process is completed using TypeScript's type information, avoiding the need for separate external formats like OpenAPI. For more details, see Automatic Generation.
  • URLs used in Pages are also automatically generated in a type-safe manner, following a policy of providing types wherever possible.

Routes-driven development (RDD)

Since routing holds the most information in web systems, we recommend starting development from routing to maintain system consistency.

Multiple central Routing information is generated from a single Routes information:

  • Server Routing information
  • SPA Routing information for browsers
  • Resource information available in REPL
  • Endpoint information within the system

Resource-View-Routes (RVR)

Our framework does not follow the MVC design pattern. Although MVC is a familiar and straightforward concept for many, our framework intentionally avoids using Controllers.

This is because Controllers, despite being files where one might want to write logic, are concepts that deal with web information. This conflict can lead to issues like the so-called Fat Controllers. To counter this, many people tend to delegate processing from Controllers to other classes.

In our system, Resources are somewhat similar to Controllers but do not carry web-related information, positioning them broadly as Models. These files can contain a lot of logic according to use cases. If necessary, developers can consider other concepts (similar to Models in MVC) for commonalities.

As a result, many functions traditionally handled by Controllers have been moved to Routes:

  • Permission checks for access (Middleware)
  • Validation of values sent from the client (Zod)
  • Handling of Mass Assignment vulnerabilities (Zod)

Routes

Routes are defined using a custom DSL, structuring the basic content around corresponding Resources.

While we introduce this below, some documentation may not be fully prepared, and specifications might be adjusted. For the latest usage examples, see example/tasks.

Router.resources

The resources method defined in Router sets up endpoints to convey request information to Resources.

At this point, Zod's type information is assigned to each action, ensuring that only schema-validated data is handled by Resources. This means input validation is automated, ensuring only specified data is received by the server, enhancing robustness.

// Defines CRUD for a single Resource corresponding to the `/tasks` path.
r.resources('tasks', {
  name: 'tasks', // The name of the resource (TasksResource interface is automatically generated)
  actions: crud(), // Defines typical actions
  construct: {
    // Specifies Zod schemas to define accepted data
    create: { schema: taskCreateWithTagsSchema },
    update: { schema: taskUpdateWithTagsSchema },
  },
})

Taking the tasks resource as an example, the default routing would look like this, defined all at once by the crud() function:

action method path type page Main Purpose
index GET /tasks true List view
show GET /tasks/$id true Detail view
build GET /tasks/build true New creation view
edit GET /tasks/$id/edit true Edit view
list GET /tasks.json json Fetch list as JSON
load GET /tasks/$id.json json Fetch details as JSON
create POST /tasks/ Create action
update PUT,PATCh /tasks/$id Update action
delete DELETE /tasks/$id Delete action

For example, the edit action for /tasks would be /tasks/$id/edit (where $id is a placeholder).

Besides crud(), there's also an api() function, which only defines list, load, create, update, and delete.

Both crud() and api() can be filtered with common arguments:

crud({ only: ['index', 'load'] }) // Only defines index and load
api({ except: ['list', 'load'] }) // Defines create, update, delete, excluding list and load

crud('index', 'load') // Only defines index and load (syntax sugar for 'only')

Furthermore, actions can not only be the return values of utilities like crud() but also custom-defined. For example, you can add a custom action done like this:

r.resources({
  ...
  actions: [...crud(), { action: 'done', path: '$id/done', method: 'post', type: 'json' }],
  ...
})

Router.pages

This method is for creating pages unrelated to Resources.

r.pages('/', ['/', '/about']) // Defines routing for `/` and `/about`

Others

  • scope: A utility for creating routing hierarchies (calls sub internally)
  • layout: Defines layouts for ReactRouter
  • sub: Creates a child Router

Resource

Resources are based on the REST concept, allowing developers to freely create necessary methods. These methods can be called as actions from Routes.

  • Automatic tests for Models are possible.
  • Resources can be easily called from a REPL, making it simple to verify logic.
  • Being broadly positioned as Models, it's fine to write a lot of

logic directly in them.

After defining Routes, running npm run bistrio:gen will automatically generate corresponding interfaces in .bistrio/resources. Using these types to implement actual Resources ensures smooth operation.

Create a directory matching the URL path hierarchy in server/resources and create a resource.ts file.

For example, the Resource for /tasks corresponds to the file server/resources/tasks/resource.ts. The content looks like this, with the utility function defineResource provided to assist in creation.

import { CustomActionOptions } from '@/server/customizers'
import { TasksResource } from '@bistrio/resources'

//...

export default defineResource(
  () =>
    ({
      // Create methods corresponding to each action name
      list: async (params): Promise<Paginated<Task>> => {
        return {
          //...
        }
      },

      load: async ({ id }): Promise<Task> => {
        // This is an example using prisma
        const task = await prisma.task.findUniqueOrThrow({ id })
        return task
      },

      // ...
      done: async ({ id }) => await prisma.task.update({ where: { id }, data: { done: true } }),
    }) as const satisfies TasksResource<CustomActionOptions>, // This specification makes the specific type available externally
)

For a more practical example, see example/tasks/server/resources/tasks/resource.ts.

When creating a Resource, keep the following points in mind:

  • The TaskResource type is a generic type that can specify custom argument types. Specify types defined by the system, like CustomActionOptions.
  • Add as const satisfies TasksResource<CustomActionOptions> to ensure the return of a specific type.

About CustomActionOptions

Processes like extracting user information from sessions are not performed within Resources. Since such processes are common across most parts of the system, they are handled before calling an action in server/customizers/index.ts's createActionOptions. Customize this content according to your application.

You can implement using the req object from the ctx variable, which is derived from Express. This return value is set as an optional argument for each action in the resource, making it available within the action.

export type CustomActionOptions = {
  user?: User
  admin?: {
    id: number
    accessedAt: Date
  }
} & ActionOptions // It is also required to be of type ActionOptions

export const createActionOptions: CreateActionOptionFunction = (ctx) => {
  const customActionOptions: CustomActionOptions = buildActionOptions({ user: ctx.req.user as User })

  if (ctx.params.adminId) {
    // For example, if it's an admin, additional specific information is included
    customActionOptions.admin = {
      id: Number(ctx.params.adminId),
      accessedAt: new Date(),
    }
  }

  return customActionOptions
}

For example, if you add a load action to a Resource, it will be set as the second argument and available for use. If there are no arguments, only CustomActionOptions is set as the argument.

      load: async ({ id }, options: CustomActionOptions): Promise<Task> => {
        // You can write processing using options
      },

View

Views are written in JSX. In this framework, server Resources can be manipulated through RenderSupport. With the introduction of Suspense in React 18, there's no need to write code heavily reliant on useEffect.

Views are conventionally called Pages in frontend JS.

Create a directory in universal/pages matching the URL path hierarchy and create files with matching names.

For example:

  • /about: universal/pages/about.tsx
  • / : universal/pages/index.tsx (index is a special name indicating /)
  • /test/mypage: universal/pages/test/mypage.tsx

RenderSupport

When implementing a Page, you need to use data from the server. At this time, information is obtained through RenderSupport.

For instance, to call the load action of the tasks resource, you would write:

import { useRenderSupport } from '@bistrio/routes/main'

// ...

function Task({ id }: { id: number }) {
  const rs = useRenderSupport()
  const task = rs.suspendedResources().tasks.load({ id }) // Communicates to call the load action of tasks resource
  // rs.suspendedResources() retrieves stubs of resources adapted for Suspense.

  return <>{/* ... */}</>
}
  • Use useRenderSupport placed in the automatically generated '@bistrio/routes/main' (the framework does not provide a fixed type).
  • If not using Suspense, calling rs.resources() returns an implementation that gives a Promise.

REPL

Running npm run console starts the REPL. You can call each resource via the global variable resources.

For example, you can test the load action of the tasks resource like this:

$ npm run console

> [email protected] console
> DEBUG=-bistrio:console NODE_ENV=development dotenv -e .env.development -- node --import ./dist/server/console.js

Welcome to Node.js v20.10.0.
Type ".help" for more information.
> await resources.tasks.load({id: 1})
{
  id: 1,
  title: 'Test1',
  description: 'Test1 Description',
  done: false,
  createdAt: 2023-12-23T05:45:07.584Z,
  updatedAt: 2024-01-28T07:57:17.471Z,
  tags: [ 'tag1', 'tag2' ]
}
>

Automatic Generation

SPA Routing and Server Routing

Since everything is automatically generated from common Routes, there's no need to worry about aligning multiple routings between the client and server.

Stubs for Client Use

Automatically created from Routes and Resource information, there's no need to be concerned about generating stubs that match server-side code.

Endpoint Information

Endpoint information used in hyperlinks, etc., is automatically generated from Routes, so following the types ensures no broken links.

Directory Structure

The directory structure during development is as follows. For more details, check the example implementation provided in example/tasks.

  • .bistrio: Automatically generated code is placed here
  • bin: Commands
  • config: Configuration (usually not frequently modified)
  • public: Static files published on the web
  • server: Server-side code
    • config: Configuration
    • resources: Directory for placing Resources
    • middleware.ts: Implementation of Middleware called on the server
  • universal: Common code for server and client (note: this is published to the browser)
    • config: Configuration
    • pages: Directory for placing JSX
    • routes: Directory for placing Routes
    • middleware.ts: Interface for Middleware used in routes

bistrio's People

Contributors

ms2sato avatar dependabot[bot] avatar

Stargazers

ugajin avatar

Watchers

 avatar  avatar

bistrio's Issues

Unexpected path join

      scope(v1Router, '/licenses/$key', (licenseRouter) => {
        licenseRouter.resources('schedules', {

created path "/licenses/$keyschedules" expected "/licenses/$key/schedules"

remove unused codes

  • [ ] remove access for application/x-www-form-urlencoded, multipart/form-data
  • arrange RenderSupport
  'application/x-www-form-urlencoded': arrangeFormInput,
  'multipart/form-data': arrangeFormInput,

Closed:

export type RenderSupport<RS extends NamedResources> = {
  ...
  readonly invalidState: InvalidState | undefined
  invalidStateOr: <T>(source: T | (() => T)) => InvalidStateOrDefaultProps<T>

optimize dev

for generator on bistrio:gen

  • output file hash? text for fs.writeFileSync(out, text)
  • check hash before writeFile

for filemap on build:client

  • output file hash on URLMapPlugin
  • check hash before writeFile

RenderSupport must have params ?

seParams() is usable on CSR and SSR.
Now server's RenderSupport#params is on express request parameters, different values for SSR/CSR.

  const PageAdapter = ({ Page }: { Page: PageNode }) => {
    const params = useParams()
    const rs = useRenderSupport<R>()
    rs.params = params
    return <Page />
  }

Fix default setting on routes creation

temporary fix https://github.com/ms2sato/bistrio/releases/tag/v0.6.5
extension switch (only) and action names "load", "list" is added for get json request of api


  • GET / HTML(page) and JSON
  • GET /$id HTML(page) and JSON
  • GET /build HTML(page)
  • GET /edit HTML(page)
  • POST / JSON
  • PUT/PATCH /$id JSON
  • DELETE /$id JSON

GET / and GET /$id is point of this issue.
switch page or resource by any conditions. Accept Header or extensions or XHR or Content-Type Header(Support)

Specifications

Adapter#override is strong interceptor more than page/Resource.

  • Accept Header
    • application/json only
      • Resource process( Exception if no exist)
    • text/html only
      • page process( Exception if no exist)
  • extensions
    • .json
      • Resource process( Exception if no exist)
    • .html
      • page process( Exception if no exist)
    • .*
      • Resource process( Exception if no exist)
  • Not specify from client, auto selection
    • Content-Type Header
      • application/json
        • Resource process( Exception if no exist) ※ will fallback page of .json(static json file)
      • text/html maybe not set
        • page process (Fallback to Resource and response is JSON)
    • X-Requested-With
      • if exist
        • Resource process (Fallback to page)
      • if not exist
        • page process(Fallback to Resource)

Consideration

extensions

Can specify one format only

const task = rs.suspendedResources().task.show({ id, format: 'json' }) // fetch to /task/[id].json

Accept Header

Although it matches the specifications best, it's not very familiar to users.
Can specify multiple formats and default is multiple formats

const task = rs.suspendedResources().task.show({ id }, { headers: { 'Accept': 'application/json' }})

XHR

Not specify format, Can be used for weak conditions of selection(May not be page HTML)

const task = rs.suspendedResources().task.show({ id }, { headers: { 'X-Requested-With': 'XMLHttpRequest' }})

Content-Type

For Request body 's specifications, but expected same one in response. Can be used for weak conditions of selection
Can specify one format only.

const task = rs.suspendedResources().task.show({ id }, { headers: { 'Content-Type': 'application/json', }})

generate typesafe link to urls

before:

<Link to={`/tasks/${task.id}`}>

after:

generate task_path function on bistrio:genere process

<Link to={task_path(task)}>

Change default dir name

  • isomorphic to universal ?
  • isomorphic/views to isomorphic/pages ?
  • server/endpoint to server/resources or any ?

universal/pages
server/resources

"Update headless: true" for warning on test

    In the near feature `headless: true` will default to the new Headless mode
    for Chrome instead of the old Headless implementation. For more
    information, please see https://developer.chrome.com/articles/new-headless/.
    Consider opting in early by passing `headless: "new"` to `puppeteer.launch()`
    If you encounter any bugs, please report them to https://github.com/puppeteer/puppeteer/issues/new/choose.

resource method cannot return null

export default defineResource((_support, _options) => ({
  user: (option?: opt<CustomMethodOption>): User => {
    return option?.body.user // not working
  },
}))
export default defineResource((_support, _options) => ({
  user: (option?: opt<CustomMethodOption>): User => {
    return option?.body.user || { username: 'unknown', id: 0, role: 1, createdAt: new Date(), updatedAt: new Date() }
  },
}))

Unify interface for Response on client and server

onSuccess logic may be isomorphic

on client side navigate by useSubmit

  const navigate = useNavigate()
  const rs = useRenderSupport()

  const props: UseSubmitProps = {
    source: { title: '', description: '', tags: [] },
    action: {
      modifier: (params) => rs.resources().task.create(params),
      onSuccess: () => navigate(`/tasks`, { purge: true }), // <----- this line
    },
    schema: formSchema,
  }

on server side redirect on Adapter interface

export type Responder<Opt = unknown, Out = unknown, Src = unknown> = {
    success?: (ctx: ActionContext, output: Out, option?: Opt) => unknown | Promise<unknown>; // <----- this line
    invalid?: (ctx: ActionContext, err: ValidationError, source: Src, option?: Opt) => void | Promise<void>;
    fatal?: (ctx: ActionContext, err: Error, option?: Opt) => void | Promise<void>;
};

Auto generate Resource name

Router#resource function and Action's specification.

  • /tasks to tasks
  • /tasks/$id to tasks_$id ( can task ? but /task is generated same name ... )
  • /users/$userId/tasks to users_$userId_tasks

tasks.index tasks.show({id}) tasks_$id.index users_$userId_tasks.index users_$userId_tasks.show({id})

BUT
name is used to named_endpoints, $ cannot use so easy...

pageRouter.layout({ element: TaskLayout }).pages('/tasks', ['/', '$id', 'build', '/$id/edit'])
// pages.tasks.index, pages.tasks.$id, pages.tasks.build, pages.tasks.$id_edit

test

  • test1
  • test2
  • test3

add useNavigate navigate function option

ref #43

const navigate = useNavigate()

navigate('/tasks', { purge: true }) // purge all suspended cache(already implemented)

navigate('/tasks', { purge: ['api_tasks', 'api_users'] }) // purge suspended cache on named resource 
navigate('/tasks', { purge: 'api_tasks' }) // purge suspended cache on named resource 
navigate('/tasks', { purge: { only: ['api_tasks', 'api_users']  } }) // purge suspended cache on named resource 
navigate('/tasks', { purge: { only: 'api_tasks'  } }) // purge suspended cache on named resource 

navigate('/tasks', { purge: { except: ['api_tasks', 'api_users']  } }) // purge suspended cache except for  named resource 
navigate('/tasks', { purge: { except: 'api_tasks'  } }) // purge suspended cache except for  named resource 

.bistrio/filemap.json key must not be null

for example example/tasks/.bistrio/filemap.json

"null" key is included. but not needed

{
  "js": {
    "main": "main.71ba2876d7db738d7bd2.bundle.js",
    "admin": "admin.42a1ca9b2e8021b8a6c2.bundle.js",
    "null": "isomorphic_components_UserLayout_tsx.f8841d9e2f4036509d19.bundle.js",
    "views": "views.7ae0ed794290b393ba57.bundle.js",

filemap.json must include javascript filename for <script>

for windows

server/endpoint dynamic routes name must not format of :something
for example [something] as Next.js

Maybe configuration based change format

fix npm run console

without resource process

main $ npm run console

> [email protected] console
> NODE_ENV=development dotenv -e .env.development -- node  -r ./server/support/tsnode_register -r ./server/support/console.ts

Welcome to Node.js v18.13.0.
Type ".help" for more information.
> console error: module: '.../bistrio/example/tasks/server/endpoint/resource' is not found
  • write specs

[Client]Resource for client side

Purpose

  • Different logic on Client only(Not same on Server ).
    • ex. Client only data connection for firebase

Spec

  • root: /client/endpoint
  • filename: resource.ts

Impl

  • create flag on generator phase: stub or provided, default is stub
  • ClientGenretateRouter is switch Resource implementation

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.