Segment
Overview
Commonly used back-end frameworks that use the Controller-Service-Repository pattern implement the following hierarchical structure for the server code:
- Back-end sever - the highest level of the hierarchy, made of controllers.
- Controllers - the second level of the hierarchy, made of handlers.
- Handlers - the lowest level of the hierarchy, they perform request processing.
(We skip services since they donβt play a role in the hierarchy.)
Vovk.ts adds a new level to this hierarchy in between the back-end server and controllers:
- Back-end server - the highest level of the hierarchy, made of segments.
- Segments - the second level of the hierarchy, made of controllers.
- Controllers - the third level of the hierarchy, made of handlers.
- Handlers - the lowest level of the hierarchy, they perform request processing.
Each segment controls a specific path such as /api/foo
, /api/bar
etc. and compiled as a separate serverless function. This allows to split the back-end code into multiple smaller βback-endsβ that are responsible for a specific area of the app and similar to how the front-end code is split into multiple pages in Next.js. Segments are implemented with the help of Next.js optional catch-all segmentsβ and initialised by calling initSegment
function in the route.ts file that is stored at [[β¦slug]] folders.
Eeach segment can have its own set of features, solving variety of concerns:
- For back-end segment code that isnβt supposed to provide RPC you can disable schema emission completely by setting
emitSchema
tofalse
. - For back-end segment code that should secure its implementation details by hiding validation data you can set
exposeValidation
tofalse
. At this case the RPC functions wouldnβt perform the client-side validation and the types generated for other programmming languages will be missing. All other features remain the same and the TypeScript inference is going to work as expected. - You can turn the segment into so-called static segment to serve data generated on build time, such as OpenAPI specification, historical data, etc.
- Using
onError
you can perform different error logging.
In development mode (when NODE_ENV
env variable is set to "development"
) each segment provides _schema_
endpoint that serves the schema of the segment and theyβre invoked by dev CLI to retrieve the schema of the segment and build corresponding JSON files at .vovk-schema/ folder. This approach allows to skip importing native Node.js modules on the Next.js side and use export const runtime = 'edge'
in the route.ts file with no errors even in development mode.
Segments also solve code inspection and reverse-engineering concerns in complex apps when Vovk.ts RPC is used. Since back-end schema files are imported by the client-side code to build RPCs, the client-side code can be reverse-engineered to get the schema of the back-end parts that are not supposed to be exposed (for example customerβs area shouldinβt include schema for the admin area). This problem is solved by introducing segmented client that splits the RPC client into separate chunks, each imports its corresponding schema file and doesnβt import others. For example, importing @/client/foo
the client-side gets access to the schema of the foo
segment stored in .vovk-schema/segments/foo.json file but doesnβt get access to the schema of the bar
segment stored in .vovk-schema/segments/bar.json file.
Segment priority
In case if you have multiple segments, more nested one is going to have higher priority. For example, if you have the following segments:
- /src/app/api/[[β¦slug]]/ for the root segment
- /src/app/api/foo/[[β¦slug]]/ for
foo
segment - /src/app/api/foo/bar/[[β¦slug]]/ for
foo/bar
segment
Then request to /api/foo/bar is going to have the highest priority and is going to be handled by the foo/bar
segment. If the request doesnβt match the foo/bar
segment but matches the foo
segment, itβs going to be handled by the foo
segment. If the request doesnβt match the foo
segment, itβs going to be handled by the root segment.
You can change the API folder name from api
to any other name by modifying config option rootEntry
.
Creating segments
Vovk.ts implements the segments via βOptional Catch-all Segmentββ defined as route.ts file in /src/app/api/[[β¦slug]]/ for root segment or /src/app/api/segment-name
/[[β¦slug]]/ folder for multi-segment apps ([[β¦slug]] is the folder name). It exports route handlersβ such as GET
, POST
etc, created by initSegment
function. The file can also export any of the Next.js segment config optionsβ such as runtime
or maxDuration
to determine behaviour of the Next.js route. If a segment is supposed to be available at front-end for RPC calls, it should also export Controllers
type that is used to type the generated TypeScript RPC library.
The code generator as well as this documentation uses vovk
as the slug name but it can be named in any valid way since itβs never used to determine something in the code.
A segment is initialised by calling initSegment
function in the route.ts file. The function takes an object with the following properties:
controllers
- an object with the controllers that are going to be used in the segment. The keys of the object define the names of the generated RPC objects and the values are the controllers.segmentName
- the name of the segment. By default itβs an empty string that defines the root segment described below.emitSchema
- a boolean value that defines whether to emit the schema for the segment. By default itβstrue
. If set tofalse
, the schema for the segment is not going to be emitted.exposeValidation
- a boolean value that defines whether to expose the validation data for the segment. By default itβstrue
. If set tofalse
, the validation data is not going to be exposed.onError
- a function that is going to be called when an error occurs in the segment. The function takes an object with the following properties:error: HttpError
- the error that occurred.request: NextRequest
- the request object that allows to get headers, URL, etc.
The root segment
npx vovk new segment # will create a new segment at src/app/api/[[...vovk]]/route.ts
For simple single page apps you can use the root segment as the only segment. In this case, the back-end code of the app is going to be bundled into a single serverless function when deployed.
Here is an example of a route.ts file for a single-segment app:
import { initSegment } from 'vovk';
import UserController from '../../modules/user/UserController';
import PostController from '../../modules/post/PostController';
// run code on the edge network
export const runtime = 'edge';
// maximum duration of request in seconds, useful for long-running JSONLines requests
export const maxDuration = 300;
const controllers = {
UserRPC: UserController,
PostRPC: PostController,
};
// export the controllers type to be used in the client code
export type Controllers = typeof controllers;
// export the Next.js route handlers
export const { GET, POST, PUT, DELETE } = initSegment({
controllers,
});
Schema for the root segment is going to be stored at .vovk-schema/root.json file.
The root
name is used for file naming purposes only. In all other cases, including configuration, the root segment
name is going to be an empty string.
Multiple segments
You can create multiple segments in your app to split the back-end code into multiple serverless functions. There might be multiple reasons for that:
- Split the back-end code into multiple serverless functions to reduce the size of the function bundle.
- Divide the back-end code into multiple areas e. g. root,
admin
,customer
,customer/public
etc. - Support different versions of the same API (e.g.
v1
,v2
). - Create a static segment for OpenAPI spec, historical data, etc.
Each segment is located in a nested folder determining the API path and the segment name. For example, a segment located in /src/app/api/segment-name
/[[β¦slug]]/ folder is going to be served at /api/segment-name
path. The level of nesting is unlimited.
For non-root segments initSegment
function requires to provide segment name as segmentName
option.
// ...
export const { GET, POST, PUT, DELETE } = initSegment({
segmentName: 'foo',
controllers,
});
The schema for the segment foo
is going to be stored at .vovk-schema/foo
.json file.
For segments of deeper nesting such as /src/app/api/foo/bar/baz
/[[β¦slug]]/ the segment name is going to be foo/bar/baz
and the schema is going to be stored at .vovk-schema/foo/bar/baz
.json file.