API Routes


While we think that using createServerData$ is the best way to write server-side code for data needed by your UI, sometimes you need to expose API routes.

This are a bunch of reasons for wanting to do this:

  • You have additional clients that want to share this logic
  • You want to expose a GraphQL or TRPC endpoint
  • You want to expose a public facing REST API
  • You need to write webhooks or auth callback handlers for OAuth
  • You want to have URLs not serving HTML, but other kinds of documents like PDFs or images

SolidStart makes it easy to write routes for these us cases.

Writing an API Route

API routes are just like any other route and follow the same filename conventions as pages. The only difference is in what you export from the file. Instead of exporting a default Page component and a routeData function, API Routes export functions that are named after the HTTP method that they handle.

routes/api/students.ts
tsx
// handles HTTP GET requests to /api/students
export function GET() {
return new Response("Hello World");
}
 
export function POST() {
// ...
}
 
export function PATCH() {
// ...
}
 
export function DELETE() {
// ...
}
routes/api/students.ts
tsx
// handles HTTP GET requests to /api/students
export function GET() {
return new Response("Hello World");
}
 
export function POST() {
// ...
}
 
export function PATCH() {
// ...
}
 
export function DELETE() {
// ...
}

These functions can also sit in your UI routes besides your component. They can handle non-GET HTTP requests for those routes.

routes/students.tsx
tsx
export function POST() {
// ...
}
 
export function routeData() {
// ...
}
 
export default function Students() {
return <h1>Students</h1>;
}
 
routes/students.tsx
tsx
export function POST() {
// ...
}
 
export function routeData() {
// ...
}
 
export default function Students() {
return <h1>Students</h1>;
}
 

Warning: A route can only export either a default Page component or a GET handler. You cannot export both.

Implementing an API Route handler

An API route gets passed an APIEvent object as its first argument. This object contains:

  • request: the Request object representing the request sent by the client
  • params: object that contains the dynamic route parameters, eg. for /api/students/:id, when user requests /api/students/123 , params.id will be "123"
  • env: the environment context, environment specific settings, bindings
  • fetch: an internal fetch function that can be used to make requests to other API routes without worrying about the origin of the URL.

An API route is expected to return a Response object.

Lets look at an example of an API route that returns a list of students in a given house, in a specific year:

routes/api/[house]/students/year-[year].ts
tsx
import { APIEvent, json } from "solid-start/api";
import hogwarts from "./hogwarts";
 
export async function GET({ params }: APIEvent) {
console.log(`House: ${params.house}, Year: ${params.year}`);
const students = await hogwarts.getStudents(params.house, params.year);
return json({ students })
}
routes/api/[house]/students/year-[year].ts
tsx
import { APIEvent, json } from "solid-start/api";
import hogwarts from "./hogwarts";
 
export async function GET({ params }: APIEvent) {
console.log(`House: ${params.house}, Year: ${params.year}`);
const students = await hogwarts.getStudents(params.house, params.year);
return json({ students })
}

Session managmment

As HTTP is a stateless protocol, for awesome dynamic experiences, you want to know the state of the session on the client. For example, you want to know who the user is. The secure way of doing this is to use HTTP-only cookies. You can store session data in them and they are persisted by the browser that your user is using.

We expose the Request object which represents the user's request. The cookies can be accessed by parsing the Cookie header in the client. Let's look at an example of how to use the cookie to identify the user:

routes/api/[house]/admin.ts
tsx
import { APIEvent, json } from "solid-start/api";
import { parseCookie } from "solid-start";
import hogwarts from "./hogwarts";
 
export async function GET({ request, params }: APIEvent) {
const cookie = parseCookie(request.headers.get("Cookie") ?? "");
const userId = cookie['userId'];
if (!userId) {
return new Response("Not logged in", { status: 401 });
}
const houseMaster = await hogwarts.getHouseMaster(params.house);
if (houseMaster.id !== userId) {
return new Response("Not authorized", { status: 403 });
}
return json({
students: await hogwarts.getStudents(params.house, params.year)
})
}
routes/api/[house]/admin.ts
tsx
import { APIEvent, json } from "solid-start/api";
import { parseCookie } from "solid-start";
import hogwarts from "./hogwarts";
 
export async function GET({ request, params }: APIEvent) {
const cookie = parseCookie(request.headers.get("Cookie") ?? "");
const userId = cookie['userId'];
if (!userId) {
return new Response("Not logged in", { status: 401 });
}
const houseMaster = await hogwarts.getHouseMaster(params.house);
if (houseMaster.id !== userId) {
return new Response("Not authorized", { status: 403 });
}
return json({
students: await hogwarts.getStudents(params.house, params.year)
})
}

This is a very simple example and quite unsecure, but you can see how you can use cookies to read and store session data. Read the session documentation for more information on how to use cookies for more secure session management.

You can read more about using HTTP cookies in the MDN documentation

Exposing a GraphQL API

SolidStart makes it easy to implement a GraphQL API. The graphql function takes a GraphQL schema and returns a function that can be used as an API route handler. TODO: Implementation

routes/api/graphql.ts
tsx
import { APIEvent } from "solid-start/api";
const graphql = (schema: string, resolvers: any) =>
(event: APIEvent) => { return new Response("GraphQL Response") }
 
const schema = `
type Query {
hello: String
}
`;
 
const handler = graphql(schema, {
Query: {
hello: () => "Hello World"
}
});
 
export const get = handler;
 
export const post = handler;
 
routes/api/graphql.ts
tsx
import { APIEvent } from "solid-start/api";
const graphql = (schema: string, resolvers: any) =>
(event: APIEvent) => { return new Response("GraphQL Response") }
 
const schema = `
type Query {
hello: String
}
`;
 
const handler = graphql(schema, {
Query: {
hello: () => "Hello World"
}
});
 
export const get = handler;
 
export const post = handler;
 

Exposing a TRPC Server route

SolidStart makes it easy to expose a TRPC server route. You can use the trpc function from solid-start/api to create a TRPC API route. The trpc function takes a TRPC server and returns a function that can be used as an API route handler.

This is your router, put it in a separate file so that you can export the type for your client.

lib/router.ts
tsx
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
hello: t.procedure.input(z.string().nullish()).query(({ input }) => {
return `hello ${input ?? 'world'}`;
}),
});
lib/router.ts
tsx
import { initTRPC } from '@trpc/server';
import { z } from 'zod';
const t = initTRPC.create();
export const appRouter = t.router({
hello: t.procedure.input(z.string().nullish()).query(({ input }) => {
return `hello ${input ?? 'world'}`;
}),
});

Here is a simple client that you can use in your routeData function to fetch data from your TRPC server. You can also use the proxy in createServerData$ and createServerAction$ functions, but its usually better to just use it in a createResource or createRouteData function.

lib/trpc.ts
tsx
export type AppRouter = typeof appRouter;
import {
createTRPCClient,
createTRPCClientProxy,
httpBatchLink,
loggerLink,
} from '@trpc/client';
import type { AppRouter } from "./router.ts";
const client = createTRPCClient<AppRouter>({
links: [loggerLink(), httpBatchLink({ url: "/api/trpc" })],
});
export const proxy = createTRPCClientProxy(client);
lib/trpc.ts
tsx
export type AppRouter = typeof appRouter;
import {
createTRPCClient,
createTRPCClientProxy,
httpBatchLink,
loggerLink,
} from '@trpc/client';
import type { AppRouter } from "./router.ts";
const client = createTRPCClient<AppRouter>({
links: [loggerLink(), httpBatchLink({ url: "/api/trpc" })],
});
export const proxy = createTRPCClientProxy(client);

And finally, the API route that acts as the TRPC server.

routes/api/trpc.ts
tsx
import { APIEvent } from "solid-start/api";
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from "~/lib/router";
export const get = (event: APIEvent) =>
fetchRequestHandler({
endpoint: '',
req: event.request,
router: appRouter,
createContext: () => ({}),
});
export const post = (event: APIEvent) =>
fetchRequestHandler({
endpoint: '',
req: event.request,
router: appRouter,
createContext: () => ({}),
});
routes/api/trpc.ts
tsx
import { APIEvent } from "solid-start/api";
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from "~/lib/router";
export const get = (event: APIEvent) =>
fetchRequestHandler({
endpoint: '',
req: event.request,
router: appRouter,
createContext: () => ({}),
});
export const post = (event: APIEvent) =>
fetchRequestHandler({
endpoint: '',
req: event.request,
router: appRouter,
createContext: () => ({}),
});

Learn more about TRPC here.