
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
Next Solid Step towards a more performant web - A full-stack SolidJS framework for building modern web applications with file-based routing, SSR, and built-in security.
[npx | yarn dlx | pnpm dlx | bunx] @varlabs/create-solidstep@latest my-app
cd my-app
[npm | yarn | pnpm | bun] install
[npm | yarn | pnpm | bun] run dev
page.tsx - Page componentlayout.tsx - Layout wrapperloading.tsx - Loading state (Streaming - optional)error.tsx - Error boundary (optional)not-found.tsx - 404 page (root only - optional)route.ts - API route handlermiddleware.ts - Request middlewareA route is defined by either the presence of a page.tsx or route.ts file in a directory.
Similar to NextJS, routes are not indexed if they have a '_' placed at the beginning of the name
Configure your app in app.config.ts:
import { defineConfig } from 'solidstep';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
server: {
preset: 'node',
},
plugins: [
{
type: 'client', // or 'server' or 'both' - depends on where you want to use the plugin
plugin: tailwindcss()
}
],
});
You can customize Vite settings for both client and server builds.
When trying to configure absolute path imports
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}
app.config.ts to ensure it works during build and runtime:import { defineConfig } from 'solidstep';
import { resolve } from 'node:path';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
server: {
preset: 'node',
},
vite: {
resolve: {
alias: {
'@': resolve(__dirname, '.'),
},
},
},
});
my-app/
├── app/
│ ├── page.tsx # Home page (/)
│ ├── layout.tsx # Root layout
│ ├── middleware.ts # Request middleware
│ ├── about/
│ │ └── page.tsx # About page (/about)
│ ├── (admin)/
│ | └── dashboard/
│ | └── page.tsx # Group route (/dashboard)
│ └── blog/
│ ├── layout.tsx # Blog layout
│ ├── page.tsx # Blog index (/blog)
│ └── [slug]/
│ └── page.tsx # Dynamic route (/blog/:slug)
├── public/
│ └── favicon.ico
├── app.config.ts
└── package.json
Wrap multiple pages with shared UI:
export default function BlogLayout(props: { children: any }) {
return (
<div>
<nav>Blog Navigation</nav>
{props.children()}
</div>
);
}
Create a page.tsx file in any directory under app/ to define a route:
export default function HomePage() {
return <h1>Welcome to SolidStep!</h1>;
}
Similar to NextJS, only content returned by a page or route is sent to the client
Use parentheses to group routes without affecting the URL:
├── (admin)/
│ └── dashboard/
│ └── page.tsx // matches /dashboard
└── (user)/
└── profile/
└── page.tsx // matches /profile
Use square brackets for dynamic segments:
// app/blog/[slug]/page.tsx - matches /blog/my-post, /blog/another-post, etc.
export default function BlogPost(props: { routeParams: { slug: string } }) {
return <h1>Post: {props.routeParams.slug}</h1>;
}
Catch-all routes:
// app/docs/[...path]/page.tsx - matches /docs/a, /docs/a/b, etc.
Catch-all routes (Optional):
// app/docs/[[...path]]/page.tsx - matches /docs, /docs/a, /docs/a/b, etc.
Render multiple sections simultaneously:
app/
├── layout.tsx
├── page.tsx
└── @graph1/
└── page.tsx
└── @graph2/
└── page.tsx
export default function RootLayout(props: {
children: any;
slots: { graph1: any; graph2: any; };
}) {
return (
<main>
{props.children()}
<aside>
<div>{props.slots.graph1()}</div>
<div>{props.slots.graph2()}</div>
</aside>
</main>
);
}
Use defineLoader to fetch data on the server:
import { defineLoader, type LoaderDataFromFunction } from 'solidstep/utils/loader';
export const loader = defineLoader(async (request) => {
const posts = await fetchPosts();
return { posts };
});
type LoaderData = LoaderDataFromFunction<typeof loader>;
export default function BlogPage(props: { loaderData: LoaderData }) {
return (
<ul>
<For each={props.loaderData.posts}>
{(post) => <li>{post.title}</li>}
</For>
</ul>
);
}
Create type-safe server functions:
'use server';
export const createPost = async (data: { title: string }) => {
await db.posts.create(data);
return { success: true };
};
Call from client:
import { createPost } from './actions';
function CreatePostForm() {
const handleSubmit = async (e: Event) => {
e.preventDefault();
await createPost({ title: 'My Post' });
};
return <form onSubmit={handleSubmit}>...</form>;
}
Define metadata for SEO:
import { meta } from 'solidstep/utils/types';
// can also be async
export const generateMeta = meta(() => {
return {
title: {
type: 'title',
content: 'My Site',
attributes: {},
},
description: {
type: 'meta',
attributes: {
name: 'description',
content: 'My awesome site',
},
},
// manifest
manifest: {
type: 'link',
attributes: {
rel: 'manifest',
href: '/site.webmanifest',
},
},
// google fonts
'google-font-link': {
type: 'link',
attributes: {
rel: 'preconnect',
href: 'https://fonts.googleapis.com'
}
},
'gstatic-font-link': {
type: 'link',
attributes: {
rel: 'preconnect',
href: 'https://fonts.gstatic.com',
crossorigin: ''
}
},
'inter-font': {
type: 'link',
attributes: {
rel: 'stylesheet',
href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap'
}
},
// external js
'analytics-script': {
type: 'script',
attributes: {
src: 'analytics.js',
defer: true,
}
}
};
});
Intercept and modify requests:
import { defineMiddleware } from 'vinxi/http';
export default defineMiddleware({
onRequest: async (request) => {
console.log('Incoming request:', request.url);
// Modify request if needed
return request;
},
});
Configure page-level caching:
export const options = {
cache: {
ttl: 60000, // Cache for 60 seconds
},
responseHeaders: { // Custom headers for pages
'X-Custom-Header': 'MyValue',
'Cache-Control': 'public, max-age=60', // Client-side caching
},
};
ttl to 0 or omitting it will disable caching for that page.
invalidateCache and revalidatePath utilities.responseHeaders option allows you to set custom HTTP headers for the page response.Create REST endpoints:
export async function GET(request: Request, { params }: any) {
const posts = await fetchPosts();
return new Response(JSON.stringify(posts), {
headers: { 'Content-Type': 'application/json' },
});
}
export async function POST(request: Request) {
const data = await request.json();
}
Serve static files from the server-assets/ directory:
├── server-assets/
│ └── secret.txt
Access via my-app/server-assets/secret.txt URL:
const TEMPLATE_PATH = join(process.cwd(), 'server-assets', 'templates', 'template.ejs');
const template = await fs.promises.readFile(TEMPLATE_PATH, 'utf-8');
options.cache property in the page.invalidateCache utility to only invalidate paths.import { invalidateCache } from 'solidstep/utils/cache';
const action = async () => {
'use server';
...
// Invalidate cache after data mutation
await invalidateCache('/some-route');
...
return { success: true };
};
revalidatePath utility to revalidate specific paths and revalidate the frontend DOM - signaling the server action as a Single Flight Mutation query.import { revalidatePath } from 'solidstep/utils/cache';
const action = async () => {
'use server';
...
// Revalidate path after data mutation
await revalidatePath('/some-route');
...
return { success: true };
};
import { getCookie, setCookie } from 'solidstep/utils/cookies';
export const loader = defineLoader(async () => {
const userData = await getCookie();
if (!userData) {
return [];
}
const userId = userData.id;
const { data, error } = await getDocumentsByUserId(userId);
if (error || !data) {
return [];
}
return data as Document[];
});
const action = async () => {
'use server';
await setCookie('session', JSON.stringify({ id: 'user-id' }), { httpOnly: true, secure: true, maxAge: 3600 });
return { success: true };
};
import { cors } from 'solidstep/utils/cors';
const trustedOrigins = ['https://example.com', 'https://another-example.com'];
const corsMiddleware = cors(trustedOrigins);
...
const corsHeaders = corsMiddleware(origin, event.node.req.method === 'OPTIONS');
...
import { createBasePolicy, serializePolicy, withNonce } from 'solidstep/utils/csp';
let cspPolicy = createBasePolicy();
...
cspPolicy = withNonce(cspPolicy, nonce);
...
event.response.headers.set('Content-Security-Policy', serializePolicy(cspPolicy));
...
import { csrf } from 'solidstep/utils/csrf';
const trustedOrigins = ['https://example.com', 'https://another-example.com'];
const csrfMiddleware = csrf(trustedOrigins);
...
const csrfResult = csrfMiddleware(
event.node.req.method,
requestUrl,
origin,
event.node.req.headers.referer
);
if (!csrfResult.success) {
event.node.res.statusCode = 403; // Forbidden
event.node.res.end(csrfResult.message);
return;
}
import { redirect } from 'solidstep/utils/redirect';
export const loader = defineLoader(async () => {
redirect('/login');
});
// or in client
export function MyComponent() {
const handleClick = () => {
redirect('/dashboard');
};
return <button onClick={handleClick}>Go to Dashboard</button>;
}
// first define an error collection
import { createErrorFactory } from 'solidstep/utils/error-handler';
export const createError = createErrorFactory({
'db-query-error': {
message: 'Something went wrong with the database query, not idea what',
severity: 'high',
action: (error) => {
console.error('Generic DB query error', error);
throw error;
},
},
'auth-error': {
message: 'User authentication failed',
severity: 'high',
action: (error) => {
console.error('User authentication error', error);
throw error;
},
},
'service-error': {
message:
'Some service (external or internal that is interfacing with the app) failed',
severity: 'high',
action: (error) => {
console.error('Service error', error);
throw error;
},
},
});
// then use it in your loaders, actions or routes
export const loader = defineLoader(async () => {
const data = await tryCatch(fetchDataFromDB());
if (data.error) {
// handle the error using the defined error collection
createError('db-query-error').action();
// or overwrite the defaults
createError('db-query-error', {
// customize the error
message: data.error.message,
action: (error) => {
// just log it for example
console.error('Custom action for DB error', error);
},
severity: 'critical',
cause: data.error,
metadata: { query: 'SELECT * FROM users' },
}).action();
// defer the definition and the handling
const error = createError('db-query-error');
// some logic
error.action();
// or throw the error
const error = createError('db-query-error', {
cause: data.error,
});
throw error;
}
return data.result;
});
SolidStep includes a built-in Pino logger that can be configured globally:
import { defineConfig } from 'solidstep';
export default defineConfig({
server: {
preset: 'node',
},
logger: {
level: 'info',
transport: {
target: 'pino-pretty', // Use pino-pretty for human-readable logs
options: {
colorize: true
}
}
}
});
Use the logger in your code:
import { logger } from 'solidstep/utils/logger';
export const loader = defineLoader(async () => {
logger.info('Fetching posts');
try {
const posts = await fetchPosts();
logger.info(`Fetched ${posts.length} posts`);
return { posts };
} catch (error) {
logger.error('Failed to fetch posts', error);
throw error;
}
});
Logger Configuration Options:
false or undefined - Disables logging (silent mode)true - Enables default Pino loggerobject - Custom Pino configuration object Pino DocsSolidStep provides type-safe fetch wrappers for both client and server with built-in timeout and error handling:
Client-side Fetch:
import fetch from 'solidstep/utils/fetch.client';
async function fetchPosts() {
const posts = await fetch<Post[]>('/api/posts', {
method: 'GET',
MAX_FETCH_TIME: 5000,
});
return posts;
}
...
// To get full response including status, headers, etc.
const response = await fetch<Post[], false>(
'/api/posts',
{ method: 'GET' },
false
);
console.log(response.status); // HTTP status code
Server-side Fetch:
import fetch from 'solidstep/utils/fetch.server';
export const loader = defineLoader(async () => {
const data = await fetch<ApiResponse>('https://api.example.com/data', {
method: 'POST',
body: JSON.stringify({ query: 'test' }),
headers: {
'Content-Type': 'application/json',
},
MAX_FETCH_TIME: 10000,
});
return data;
});
Features:
Ensure code only runs on the server and throws an error if accessed on the client:
import 'solidstep/utils/server-only';
export const SECRET_KEY = process.env.SECRET_KEY;
export const DATABASE_URL = process.env.DATABASE_URL;
export async function queryDatabase(query: string) {
}
Use case: Import this at the top of any file that should never be used for the client (e.g., database utilities, API keys, server secrets).
import 'solidstep/utils/server-only';
export const db = createDatabaseConnection(process.env.DATABASE_URL);
If accidentally imported on the client, it will throw:
Error: This module is only available on the server side.
SolidStep supports various preloading and prefetching strategies to enhance user experience by loading data and resources ahead of time. This can significantly reduce perceived latency and improve navigation speed within your application. Solidstep does not include any preloading/prefetching by default, but you can implement your own strategies using the built-in fetch utilities and SolidJS features.
Some common strategies include:
<link rel="prefetch"> tag to hint the browser to prefetch resources for links that users are likely to click on next.export const RootLayout = (props) => {
return (
<body>
...
<NoHydration>
<script src="//instant.page/5.2.0" type="module" integrity="sha384-jnZyxPjiipYXnSU0ygqeac2q7CVYMbh84q0uHVRRxEtvFPiQYbXWUorga2aqZJ0z"></script>
</NoHydration>
</body>
);
};
import { ForesightManager } from "js.foresight";
import { onMount } from "solid-js";
export const RootLayout = (props) => {
onMount(() => {
ForesightManager.initialize({
// Configuration options
});
});
return (
<body>
...
</body>
);
};
Install fonts (for example, from Fontsource) and import into globals.css example:
@import '@fontsource-variable/dm-sans';
@import '@fontsource-variable/jetbrains-mono';
@theme inline {
--font-sans: 'DM Sans Variable', sans-serif;
--font-mono: 'JetBrains Mono Variable', monospace;
/* ... */
}
Use the package called Unpic for images. An open source and powerful tool for images on the web.
[npm | yarn | pnpm | bun] install @unpic/solid
import type { Component } from "solid-js";
import { Image } from "@unpic/solid";
const MyComponent: Component = () => {
return (
<Image
src="https://cdn.shopify.com/static/sample-images/bath_grande_crop_center.jpeg"
layout="constrained"
width={800}
height={600}
alt="A lovely bath"
/>
);
};
As SolidStep is built using Vite, it follows the same guide as stated in Vite docs regarding environment variables.
SolidStep does not include a built-in testing framework. However, we recommend setting up testing using Vitest ecosystem. You can use Vitest for unit and integration tests, and Playwright for end-to-end testing.
When testing server actions, you can use Vitest to accomplish this. Just test as you would with any other async function.
When testing pages (e2e tests), you can trigger server actions by simulating user interactions that would call those actions. If needed, you can also intercept network requests to directly test the action endpoints. Use the testing framework's capabilities to intercept the requests and ensure the responses have the expected results. If the server action returns json data, stringify it and add it to the response body as well as setting the content-type header to 'application/json'. If the action has a more complex return type, use seroval to serialize the response before sending it back.
MIT
FAQs
Next Step SolidJS Framework for building web applications.
The npm package solidstep receives a total of 0 weekly downloads. As such, solidstep popularity was classified as not popular.
We found that solidstep demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.