Skip to main content

Resource Routes in Generated Data Loaders

Resource Routes Overview

Resource routes are special routes that require resolved data rather than deferred promises. They are commonly used for API endpoints, data fetching routes, and other scenarios where immediate data resolution is required.

Configuration

routes.json Configuration

{
"/api/teams": {
"isResourceRoute": true,
"resourceUri": "teams",
"queries": {
"GetTeamsDocument": "{orgName: params.orgName}"
},
"middlewares": [
"@your-package/middleware/configurations"
]
}
}

GraphQL Query Definition

query GetTeams($orgName: String!) {
teams(orgName: $orgName) {
id
name
members {
id
name
role
}
settings {
isPublic
allowInvites
}
}
}

Generated Loader

import { json } from '@remix-run/node';
import { GetTeamsDocument } from './graphql/teams.generated';

export const loader = async ({ context, params }) => {
const loaderErrors = [];
let defaultLoaderData = {};

// Configuration middleware
const middlewareStack = [];
const { apolloClient: client } = context;

try {
// Execute queries with immediate resolution
const teamsResult = await client.query({
query: GetTeamsDocument,
variables: { orgName: params.orgName },
fetchPolicy: 'network-only',
});

defaultLoaderData = {
teams: teamsResult.data.teams
};

// Execute middleware if configured
if (options.resourceUri) {
const { configurations } = await executeConfigurationMiddleware(params);
defaultLoaderData.configurations = configurations;
}

return json({
...defaultLoaderData,
errors: loaderErrors
});

} catch (error) {
loaderErrors.push(error);
return json({ errors: loaderErrors }, { status: 500 });
}
};

Example Usage

Resource Route Component

// app/routes/api/teams.ts
import { useLoaderData } from '@remix-run/react';
import type { Team } from '~/types';

interface LoaderData {
teams: Team[];
configurations?: {
maxTeamSize: number;
features: string[];
};
errors?: Array<{ message: string }>;
}

export default function TeamsResource() {
const { teams, configurations, errors } = useLoaderData<LoaderData>();

if (errors?.length) {
return json({
error: 'Failed to load teams data',
details: errors
}, {
status: 500
});
}

return json({
teams,
settings: configurations,
timestamp: new Date().toISOString()
});
}

Consuming Component

// app/routes/teams/index.tsx
import { useEffect, useState } from 'react';

export default function TeamsPage() {
const [teams, setTeams] = useState<Team[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
async function fetchTeams() {
try {
const response = await fetch('/api/teams?orgName=acme');
const data = await response.json();
setTeams(data.teams);
} catch (error) {
console.error('Failed to fetch teams:', error);
} finally {
setLoading(false);
}
}

fetchTeams();
}, []);

if (loading) return <div>Loading teams...</div>;

return (
<div>
<h1>Teams</h1>
<div className="teams-grid">
{teams.map(team => (
<TeamCard
key={team.id}
team={team}
maxSize={data.configurations?.maxTeamSize}
/>
))}
</div>
</div>
);
}

// TeamCard component
const TeamCard = ({ team, maxSize }) => (
<div className="team-card">
<h3>{team.name}</h3>
<div className="member-count">
Members: {team.members.length} / {maxSize}
</div>
<div className="member-list">
{team.members.map(member => (
<div key={member.id} className="member">
{member.name} ({member.role})
</div>
))}
</div>
<div className="team-settings">
{team.settings.isPublic ? 'Public' : 'Private'} Team
{team.settings.allowInvites && ' • Invites Enabled'}
</div>
</div>
);

Using with Forms

// app/routes/teams/new.tsx
import { Form, useActionData } from '@remix-run/react';

export default function NewTeam() {
const actionData = useActionData();

return (
<Form method="post" action="/api/teams">
<div>
<label htmlFor="teamName">Team Name:</label>
<input
id="teamName"
name="teamName"
type="text"
required
/>
</div>
<div>
<label htmlFor="isPublic">
<input
id="isPublic"
name="isPublic"
type="checkbox"
/>
Make team public
</label>
</div>
<button type="submit">Create Team</button>
{actionData?.errors && (
<div className="error">
{actionData.errors.map(error => (
<p key={error.message}>{error.message}</p>
))}
</div>
)}
</Form>
);
}

Key Differences from Regular Routes

  1. Data Resolution

    • All data is resolved before sending response
    • Always returns json responses
    • No streaming or deferred loading
  2. Error Handling

    • Errors are collected and returned immediately
    • Appropriate HTTP status codes are set
    • Error details can be included in the response
  3. Middleware Integration

    • Middleware executes synchronously
    • All configurations are resolved before response
    • Results are included in the JSON response
  4. Response Format

    • Structured JSON responses
    • Can include metadata and configurations
    • Suitable for API consumption

Back to Index | Previous: Query Parameters Generator | Next: CSS Import and Stylesheets