Integrating MeshBase with Remix
Build performant, progressively-enhanced apps with Remix and MeshBase. Loaders, actions, nested routes—all the Remix patterns you need.
Why MeshBase + Remix?
🌐Web Standards First
Remix uses native fetch, FormData, and Web APIs. MeshBase integrates naturally with these standards.
⚡Server-Side by Default
Loaders run server-side. Fetch MeshBase content server-side for optimal performance and SEO.
🔄Progressive Enhancement
Works without JavaScript. MeshBase content renders server-side, then enhances with client-side interactions.
🎯Nested Routes
Parallel data loading with nested routes. Fetch different MeshBase content types in parallel efficiently.
What You'll Need
- ✓Remix project (v2+)
- ✓A MeshBase account with content types defined
- ✓Your MeshBase API key
Quick Start
Fetch MeshBase content in Remix loaders.
1. Configure environment
Create .env:
MESHBASE_API_URL=https://api.meshbase.io/v1
MESHBASE_API_KEY=your-api-key-here2. Create API client
Create app/lib/meshbase.server.ts:
// .server.ts ensures this only runs server-side
const API_URL = process.env.MESHBASE_API_URL;
const API_KEY = process.env.MESHBASE_API_KEY;
export interface BlogPost {
id: string;
title: string;
excerpt: string;
content: string;
coverImage?: string;
slug: string;
}
export async function fetchFromMeshBase<T>(endpoint: string): Promise<T> {
const response = await fetch(`${API_URL}${endpoint}`, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Response('Failed to fetch from MeshBase', {
status: response.status
});
}
const json = await response.json();
return json.data;
}
export async function getBlogPosts(): Promise<BlogPost[]> {
return fetchFromMeshBase('/blog-posts');
}
export async function getBlogPost(id: string): Promise<BlogPost> {
return fetchFromMeshBase(`/blog-posts/${id}`);
}3. Use in route loader
Create app/routes/blog._index.tsx:
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData, Link } from '@remix-run/react';
import { getBlogPosts } from '~/lib/meshbase.server';
export async function loader({ request }: LoaderFunctionArgs) {
const posts = await getBlogPosts();
return json({ posts });
}
export default function BlogIndex() {
const { posts } = useLoaderData<typeof loader>();
return (
<div>
<h1>Blog Posts</h1>
<div className="blog-list">
{posts.map(post => (
<article key={post.id}>
<h2>
<Link to={`/blog/${post.slug || post.id}`}>
{post.title}
</Link>
</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
</div>
);
}You're Done!
Your Remix app now fetches MeshBase content server-side. Perfect SEO, fast initial load, progressively enhanced!
Dynamic Routes
Create dynamic blog post pages.
Create app/routes/blog.$slug.tsx:
import type { LoaderFunctionArgs, MetaFunction } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { getBlogPost } from '~/lib/meshbase.server';
export async function loader({ params }: LoaderFunctionArgs) {
const post = await getBlogPost(params.slug!);
return json({ post });
}
export const meta: MetaFunction<typeof loader> = ({ data }) => {
return [
{ title: data?.post.title },
{ name: 'description', content: data?.post.excerpt }
];
};
export default function BlogPost() {
const { post } = useLoaderData<typeof loader>();
return (
<article>
<h1>{post.title}</h1>
{post.coverImage && (
<img src={post.coverImage} alt={post.title} />
)}
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}Nested Routes & Parallel Loading
Load different MeshBase content types in parallel with nested routes.
Parent route
app/routes/blog.tsx (layout):
import { Outlet } from '@remix-run/react';
export default function BlogLayout() {
return (
<div>
<header>
<h1>My Blog</h1>
{/* Sidebar, nav, etc. */}
</header>
<main>
<Outlet /> {/* Child routes render here */}
</main>
</div>
);
}Child routes load in parallel
Both loaders run simultaneously:
blog.tsx- layout datablog._index.tsx- blog post list
Waterfall Prevention
Remix loads nested routes in parallel. No request waterfalls—all MeshBase data fetches happen simultaneously!
Caching with Headers
Add cache control headers for better performance.
export async function loader({ request }: LoaderFunctionArgs) {
const posts = await getBlogPosts();
return json(
{ posts },
{
headers: {
'Cache-Control': 'public, max-age=60, s-maxage=3600'
}
}
);
}Cache-Control explained:
max-age=60- Browser cache for 60ss-maxage=3600- CDN cache for 1 hour
Actions (Creating Content)
Use Remix actions to create/update MeshBase content (admin key required).
import type { ActionFunctionArgs } from '@remix-run/node';
import { redirect } from '@remix-run/node';
export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const title = formData.get('title');
const content = formData.get('content');
// Create post via MeshBase API (use admin key!)
await fetch(`${API_URL}/blog-posts`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${ADMIN_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ title, content })
});
return redirect('/blog');
}
export default function NewPost() {
return (
<form method="post">
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create Post</button>
</form>
);
}Admin Key Required
Creating/updating content requires your MeshBase admin API key. Never expose this in client-side code—keep it server-side only!
Next Steps
- →Handle images and media
Upload and serve media files
- →Explore the full API
Filtering, sorting, pagination, and more
- →Remix Documentation
Learn more about Remix