Server-side Logic (server.js / entry.server.ts)
This guide covers how to add server-side logic to your components. Modern components use src/entry.server.ts with the Server API, while the legacy server.js with callbacks is still supported for backwards compatibility.
New components should use the modern Server API shown in this guide. The legacy callback style (module.exports.data = (context, callback) => { ... }) still works but is considered deprecated.
When do you need a server entry?
Understanding when to use server.js is crucial for building effective OpenComponents. Here's a simple decision guide:
Static components (no server entry needed)
Use static components when your content doesn't change based on user input or external data:
// src/entry.client.tsx - Static component
export default function Component() {
return <div className="banner">Welcome to our site!</div>;
}
Use cases:
- Static banners or promotional content
- Fixed navigation menus
- Copyright footers
- Terms of service links
Dynamic components (server entry required)
Use a server entry when you need to:
- Fetch data from APIs or databases
- Process user parameters
- Implement business logic
- Handle authentication or personalization
- Format or transform data
// src/entry.client.tsx - Dynamic component
export default function Component(props: {
name: string;
messageCount: number;
}) {
return (
<div>
Hello {props.name}, you have {props.messageCount} messages
</div>
);
}
// src/entry.server.ts - Provides dynamic data (modern API)
import { Server } from "oc-server";
export const server = new Server({ development: { console: true } })
.withParameters({
userId: {
type: "number",
description: "User id",
example: 1,
default: 1,
mandatory: true,
},
})
.handler(async (params, ctx) => {
const { userId } = params;
// Fetch user data and message count
ctx.setHeader("Cache-Control", "max-age=300");
return { name: "John", messageCount: 5 };
})
.action("markAsRead", async (params: { messageId: string }) => {
return { ok: true };
});
Basic Example
Here's a simple dynamic component that demonstrates the core pattern:
// src/entry.server.ts - Parameter handling with conditional logic
import { Server } from "oc-server";
export const server = new Server()
.withParameters({
name: { type: "string", default: "Guest" },
theme: { type: "string", default: "light" },
})
.handler(async (params) => {
const now = new Date();
return {
name: params.name ?? "Guest",
theme: params.theme ?? "light",
currentTime: now.toLocaleTimeString(),
cssClass: params.theme === "dark" ? "dark-theme" : "light-theme",
};
});
// src/entry.client.tsx
export default function Component(props: {
name: string;
theme: string;
currentTime: string;
cssClass: string;
}) {
return (
<div className={props.cssClass}>
<h3>Hello {props.name}!</h3>
<p>Current time: {props.currentTime}</p>
</div>
);
}
Usage: <oc-component href="http://localhost:3030/greeting?name=Alice&theme=dark"></oc-component>
Real-World Example: User Dashboard
Here's a more complex example that combines authentication, API data fetching, and error handling:
// src/entry.server.ts - User dashboard with auth and API integration
import { Server } from "oc-server";
type User = {
id: string;
name: string;
email: string;
role: "user" | "admin";
} | null;
async function getUserFromToken(token?: string): Promise<User> {
if (!token) return null;
// In real implementation: validate JWT token
return {
id: "123",
name: "John Doe",
email: "john@example.com",
role: "user",
};
}
export const server = new Server()
.withParameters({ userId: { type: "number", mandatory: true } })
.middleware(async (_params, ctx) => {
const token = ctx.requestHeaders.authorization?.replace("Bearer ", "");
const user = await getUserFromToken(token);
return { user };
})
.handler(async ({ userId }, ctx) => {
const isAuthenticated = !!ctx.state.user;
if (!isAuthenticated) {
return {
error: "Authentication required",
showLoginButton: true,
} as const;
}
try {
// In real implementation: fetch from API
// const res = await fetch(`https://api.example.com/users/${userId}`);
// const userData = await res.json();
const userData = {
name: "John Doe",
email: "john@example.com",
lastLogin: "2025-01-15",
profileComplete: true,
};
return {
user: userData,
isAuthenticated: true,
userRole: ctx.state.user?.role ?? "guest",
showLogoutButton: true,
};
} catch {
return {
error: "Failed to load user data",
isAuthenticated: true,
} as const;
}
});
Advanced Features
Error handling
import { Server, ServerError } from "oc-server";
function processData(param: string) {
return { processed: param };
}
export const server = new Server()
.withParameters({ required: { type: "string", mandatory: true } })
.handler(async ({ required }) => {
if (!required) throw new ServerError(400, "Required parameter missing");
return processData(required);
});
To prevent rendering entirely (e.g., invalid state), return nothing:
export const server = new Server().handler(async (params) => {
if ((params as any).name === "Invalid") {
return; // prevents rendering
}
return { name: (params as any).name };
});
Using context
The context object (ctx) provides access to request data and helpers:
- acceptLanguage: parsed
Accept-Languageheader - baseUrl: registry base URL
- env: registry environment
- params: validated parameters
- plugins: registry plugins
- requestHeaders: original request headers
- requestIp: client IP
- setEmptyResponse(): set the response to empty
- setHeader(name, value): set a response header
- setCookie(name, value, options?): set a cookie
- staticPath: public path for static assets
- templates: template metadata
- state: middleware-provided state
Context properties
Context aggregates request data, and the registry's context. It consists of the following fields:
{
"acceptLanguage": [
{
"code": "en",
"region": "US",
"quality": 1
},
{
"code": "en",
"quality": 0.8
}
],
"baseUrl": "http://localhost:3030/",
"env": {
"name": "local"
},
"params": {
"name": "Johnny"
},
"renderComponent": [Function],
"renderComponents": [Function],
"requestHeaders": {
"host": "localhost:3000",
"connection": "keep-alive",
"accept": "application/vnd.oc.unrendered+json",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36",
"content-type": "text/plain",
"referer": "http://localhost:3000/oc-client/0.28.5/~preview/",
"accept-encoding": "gzip, deflate, sdch",
"accept-language": "en-US,en;q=0.8",
},
"setEmptyResponse": [Function],
"setHeader": [Function],
"staticPath": "http://localhost:3030/hello-world/1.0.0/static/"
}
| Parameter | Type | Description |
|---|---|---|
acceptLanguage | array of objects | represents parsed accept-language part of the request header sorted by quality. More details here |
baseUrl | string | represents public registry base url. This is required when we want to nest components that are hosted in the same registry for client-side rendering |
env | object | represents the registry environment variables. The registry's admin can share here arbitrary data |
params | object | represents parameters extracted from the query string |
plugins | array of functions | custom functionalities that extend the registry. Note: if a component needs a plugin, it needs to be declared in the component's package.json |
renderComponent | function | Needed to do server-side nested renderings. renderComponent API |
renderComponents | function | Needed to do server-side nested renderings. renderComponents API |
requestHeaders | object | represents the original request headers. When component is rendered client side this will be what is sent from the browser, when rendered server side, using oc-client, this will contain whatever the client has been configured to forward |
setEmptyResponse | function | Sets the response to be completely empty (included container and renderInfo). Recommended is to return it: return context.setEmptyResponse(); |
setHeader | function | Needed to set custom headers. Signature is (name, value) |
setCookie | function | Needed to set cookies. Signature is (name, value, options?) where options supports all standard Express cookie options (httpOnly, secure, maxAge, etc.) |
staticPath | string | represents the path to static resources i.e. images, styles, javascript files. This is required to reference static resources to the view and already includes a final slash (this is relevant due to how S3 handles filePaths). |
renderComponent API
Signature: context.renderComponent(componentName [, options], callback)
| Parameter | Type | Mandatory | Description |
|---|---|---|---|
componentName | string | yes | The component's name to be rendered. Needs to be hosted on the same registry |
options | object | no | The options |
options.version | string | no | Default '' (latest) - the version of the component to render |
options.parameters | object | no | Component's parameters |
options.headers | object | no | Headers to be injected to the nested component |
callback | function | yes | The callback. Signature is callback(error, html) |
renderComponents API
Signature: context.renderComponents(components [, options], callback)
| Parameter | Type | Mandatory | Description |
|---|---|---|---|
components | array | yes | The components to be rendered. Need to be hosted on the same registry |
components[index].name | string | yes | The component's name to be rendered |
components[index].version | string | no | Default '' (latest) - the version of the component to render |
components[index].parameters | object | no | The components' parameters |
options | object | no | The global options |
options.parameters | object | no | Components "global" parameters. If in need to render many components with the same parameters, they can be specified "globally" instead of in each component's "parameters" field |
options.headers | object | no | Headers to be forwarded to be injected to the nested components |
callback | function | yes | The callback. Signature is callback(error, response). Response will be an array with a mix of strings of html (in case of success) and/or Error containing the details (in case of failure) |
Add static resource to the component
In this example an image (public/static_resource.png) will be our static resource.
Prepare package file
Add static to oc.files and point to the modern entries:
{
"name": "hello-world",
"version": "1.0.0",
"oc": {
"files": {
"data": "src/entry.server.ts",
"template": {
"src": "src/entry.client.tsx",
"type": "oc-template-esm"
},
"static": ["public"]
}
}
}
It is an array of names of directories. In the above example the public directory will be included inside the package.
Add image in the view template
We can add image to the component view template using img tag in which src attribute is bound to img viewModel property.
export default function Component(props: { path: string }) {
return <img src={`${props.path}public/static_resource.png`} />;
}
Update server file
To provide img parameter in our viewModel we need to update server.js. The important thing is we need to use context.staticPath to provide url to the static resources:
import { Server } from "oc-server";
export const server = new Server().handler(async (_params, ctx) => {
return { path: ctx.staticPath };
});
Performance Tips
Keep your server entry lightweight and efficient:
// ✅ Good: Cache results, use timeouts, return only needed data
export const server = new Server()
.withParameters({ userId: { type: "number", mandatory: true } })
.handler(async ({ userId }) => {
// Use caching for expensive operations
const cached = getFromCache(`user-${userId}`);
if (cached) return cached;
// Set timeout for external requests
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(), 5000);
try {
// const res = await fetch(`/api/users/${userId}`, { signal: ac.signal });
// const userData = await res.json();
// Return only what the view needs
const result = { name: "John", email: "john@example.com" };
setCache(`user-${userId}`, result, 300); // Cache for 5 minutes
return result;
} catch {
return { error: "Request timeout" } as const;
} finally {
clearTimeout(timer);
}
});
Node.js dependencies
Local development
List dependencies in your component's package.json:
{
"name": "my-component",
"dependencies": {
"lodash": "^4.17.21",
"moment": "^2.29.4",
"axios": "^1.0.0"
}
}
Then use them in src/entry.server.ts (ESM imports):
import _ from "lodash";
import dayjs from "dayjs";
import axios from "axios";
import { Server } from "oc-server";
export const server = new Server()
.withParameters({ users: { type: "string" } })
.handler(async (params) => {
const users = _.uniqBy(JSON.parse(params.users || "[]"), "id");
const currentDate = dayjs().format("YYYY-MM-DD");
return { users, currentDate };
});
Publishing considerations
When publishing, the registry may restrict dependencies for security and performance:
- Allowed: Common utilities (lodash, moment, axios)
- Restricted: File system access, network libraries, native modules
- Forbidden: Packages with security vulnerabilities
Check with your registry administrator about approved dependencies.
Common Issues
Missing return statement
// ❌ Problem - not returning data
export const server = new Server().handler(async () => {
const data = { message: "Hello" };
// Forgot to return data
});
// ✅ Solution - always return data
export const server2 = new Server().handler(async () => {
return { message: "Hello" };
});
Undefined property access
// ❌ Problem - not checking optional fields
export const server = new Server().handler(async (params: any) => {
const name = params.user.name; // Error if user is undefined
return { name };
});
// ✅ Solution - defensive programming
export const server2 = new Server().handler(async (params: any) => {
const user = params.user || {};
const name = user.name || "Anonymous";
return { name };
});
Debugging tips
- Enable browser console relay:
new Server({ development: { console: true } }) - Use console.log: Add logging to understand data flow
- Check browser network tab: Verify component requests and responses
- Validate JSON: Ensure your callback data is valid JSON
- Test parameters: Use
oc previewto test different parameter combinations
Type inference (optional)
For automatic type inference between server and client, add this to src/entry.server.ts:
declare module "oc-server" {
interface Register {
server: typeof server;
}
}
Legacy (deprecated): server.js with callbacks
This older style is still supported for existing components, but new code should use Server as shown above.
// server.js (legacy)
module.exports.data = function (context, callback) {
const name = context.params.name || "Guest";
callback(null, { name });
};
Next Steps
- Learn package.json structure - Configure your component properly
- Master the CLI - Efficient development workflow
- Publishing guide - Deploy your components
- Client-side operations - Browser integration patterns