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
return new ResponseThis Fetch API interface represents the response to a request.
var Response: new (body?: BodyInit | null | undefined, init?: ResponseInit | undefined) => Response
("Hello World"); }
// ...
}
export function PATCH() { // ...
}
export function DELETE() { // ...
}
routes/api/students.ts
tsx
// handles HTTP GET requests to /api/students
return new ResponseThis Fetch API interface represents the response to a request.
var Response: new (body?: BodyInit | null | undefined, init?: ResponseInit | undefined) => Response
("Hello World"); }
// ...
}
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 routeDatafunction routeData(): void
() { // ...
}
export default function Studentsfunction Students(): JSX.Element
() { return <h1(property) JSX.IntrinsicElements.h1: JSX.HTMLAttributes<HTMLHeadingElement>
>Students</h1(property) JSX.IntrinsicElements.h1: JSX.HTMLAttributes<HTMLHeadingElement>
>; }
routes/students.tsx
tsx
// ...
}
export function routeDatafunction routeData(): void
() { // ...
}
export default function Studentsfunction Students(): JSX.Element
() { return <h1(property) JSX.IntrinsicElements.h1: JSX.HTMLAttributes<HTMLHeadingElement>
>Students</h1(property) JSX.IntrinsicElements.h1: JSX.HTMLAttributes<HTMLHeadingElement>
>; }
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(alias) interface APIEvent
import APIEvent
, jsonA JSON response. Converts `data` to JSON and sets the `Content-Type` header.
(alias) function json<Data>(data: Data, init?: number | ResponseInit): Response
import json
} from "solid-start/api"; import hogwarts from "./hogwarts";
export async function GETfunction GET({ params }: APIEvent): Promise<Response>
({ params(parameter) params: {
[key: string]: string;
}
}: APIEvent(alias) interface APIEvent
import APIEvent
) { console.log(method) Console.log(...data: any[]): void
(`House: ${params(parameter) params: {
[key: string]: string;
}
.house}, Year: ${params(parameter) params: {
[key: string]: string;
}
.year}`); const studentsconst students: {
name: string;
house: string;
year: string;
}[]
= await hogwarts.getStudents(method) getStudents(house: string, year: string): {
name: string;
house: string;
year: string;
}[]
(params(parameter) params: {
[key: string]: string;
}
.house, params(parameter) params: {
[key: string]: string;
}
.year); return jsonA JSON response. Converts `data` to JSON and sets the `Content-Type` header.
(alias) json<{
students: {
name: string;
house: string;
year: string;
}[];
}>(data: {
students: {
name: string;
house: string;
year: string;
}[];
}, init?: number | ResponseInit): Response
import json
({ students(property) students: {
name: string;
house: string;
year: string;
}[]
}) }
routes/api/[house]/students/year-[year].ts
tsx
import { APIEvent(alias) interface APIEvent
import APIEvent
, jsonA JSON response. Converts `data` to JSON and sets the `Content-Type` header.
(alias) function json<Data>(data: Data, init?: number | ResponseInit): Response
import json
} from "solid-start/api"; import hogwarts from "./hogwarts";
export async function GETfunction GET({ params }: APIEvent): Promise<Response>
({ params(parameter) params: {
[key: string]: string;
}
}: APIEvent(alias) interface APIEvent
import APIEvent
) { console.log(method) Console.log(...data: any[]): void
(`House: ${params(parameter) params: {
[key: string]: string;
}
.house}, Year: ${params(parameter) params: {
[key: string]: string;
}
.year}`); const studentsconst students: {
name: string;
house: string;
year: string;
}[]
= await hogwarts.getStudents(method) getStudents(house: string, year: string): {
name: string;
house: string;
year: string;
}[]
(params(parameter) params: {
[key: string]: string;
}
.house, params(parameter) params: {
[key: string]: string;
}
.year); return jsonA JSON response. Converts `data` to JSON and sets the `Content-Type` header.
(alias) json<{
students: {
name: string;
house: string;
year: string;
}[];
}>(data: {
students: {
name: string;
house: string;
year: string;
}[];
}, init?: number | ResponseInit): Response
import json
({ students(property) students: {
name: string;
house: string;
year: string;
}[]
}) }
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(alias) interface APIEvent
import APIEvent
, jsonA JSON response. Converts `data` to JSON and sets the `Content-Type` header.
(alias) function json<Data>(data: Data, init?: number | ResponseInit): Response
import json
} from "solid-start/api"; import { parseCookieParse a cookie header.
Parse the given cookie header string into an object
The object has the various cookies as keys(names) => values
(alias) function parseCookie(str: string, options?: CookieParseOptions): Record<string, string>
import parseCookie
} from "solid-start"; import hogwarts from "./hogwarts";
export async function GETfunction GET({ request, params }: APIEvent): Promise<Response>
({ request(parameter) request: Request
, params(parameter) params: {
[key: string]: string;
}
}: APIEvent(alias) interface APIEvent
import APIEvent
) { const cookieconst cookie: Record<string, string>
= parseCookieParse a cookie header.
Parse the given cookie header string into an object
The object has the various cookies as keys(names) => values
(alias) parseCookie(str: string, options?: CookieParseOptions | undefined): Record<string, string>
import parseCookie
(request(parameter) request: Request
.headersReturns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header.
(property) Request.headers: Headers
.get(method) Headers.get(name: string): string | null
("Cookie") ?? ""); const userId = cookieconst cookie: Record<string, string>
['userId']; return new ResponseThis Fetch API interface represents the response to a request.
var Response: new (body?: BodyInit | null | undefined, init?: ResponseInit | undefined) => Response
("Not logged in", { status(property) ResponseInit.status?: number | undefined
: 401 }); }
const houseMasterconst houseMaster: {
name: string;
house: string;
id: string;
}
= await hogwarts.getHouseMaster(method) getHouseMaster(house: string): {
name: string;
house: string;
id: string;
}
(params(parameter) params: {
[key: string]: string;
}
.house); if (houseMasterconst houseMaster: {
name: string;
house: string;
id: string;
}
.id !== userId) { return new ResponseThis Fetch API interface represents the response to a request.
var Response: new (body?: BodyInit | null | undefined, init?: ResponseInit | undefined) => Response
("Not authorized", { status(property) ResponseInit.status?: number | undefined
: 403 }); }
return jsonA JSON response. Converts `data` to JSON and sets the `Content-Type` header.
(alias) json<{
students: {
name: string;
house: string;
year: string;
}[];
}>(data: {
students: {
name: string;
house: string;
year: string;
}[];
}, init?: number | ResponseInit): Response
import json
({ students(property) students: {
name: string;
house: string;
year: string;
}[]
: await hogwarts.getStudents(method) getStudents(house: string, year: string): {
name: string;
house: string;
year: string;
}[]
(params(parameter) params: {
[key: string]: string;
}
.house, params(parameter) params: {
[key: string]: string;
}
.year) })
}
routes/api/[house]/admin.ts
tsx
import { APIEvent(alias) interface APIEvent
import APIEvent
, jsonA JSON response. Converts `data` to JSON and sets the `Content-Type` header.
(alias) function json<Data>(data: Data, init?: number | ResponseInit): Response
import json
} from "solid-start/api"; import { parseCookieParse a cookie header.
Parse the given cookie header string into an object
The object has the various cookies as keys(names) => values
(alias) function parseCookie(str: string, options?: CookieParseOptions): Record<string, string>
import parseCookie
} from "solid-start"; import hogwarts from "./hogwarts";
export async function GETfunction GET({ request, params }: APIEvent): Promise<Response>
({ request(parameter) request: Request
, params(parameter) params: {
[key: string]: string;
}
}: APIEvent(alias) interface APIEvent
import APIEvent
) { const cookieconst cookie: Record<string, string>
= parseCookieParse a cookie header.
Parse the given cookie header string into an object
The object has the various cookies as keys(names) => values
(alias) parseCookie(str: string, options?: CookieParseOptions | undefined): Record<string, string>
import parseCookie
(request(parameter) request: Request
.headersReturns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header.
(property) Request.headers: Headers
.get(method) Headers.get(name: string): string | null
("Cookie") ?? ""); const userId = cookieconst cookie: Record<string, string>
['userId']; return new ResponseThis Fetch API interface represents the response to a request.
var Response: new (body?: BodyInit | null | undefined, init?: ResponseInit | undefined) => Response
("Not logged in", { status(property) ResponseInit.status?: number | undefined
: 401 }); }
const houseMasterconst houseMaster: {
name: string;
house: string;
id: string;
}
= await hogwarts.getHouseMaster(method) getHouseMaster(house: string): {
name: string;
house: string;
id: string;
}
(params(parameter) params: {
[key: string]: string;
}
.house); if (houseMasterconst houseMaster: {
name: string;
house: string;
id: string;
}
.id !== userId) { return new ResponseThis Fetch API interface represents the response to a request.
var Response: new (body?: BodyInit | null | undefined, init?: ResponseInit | undefined) => Response
("Not authorized", { status(property) ResponseInit.status?: number | undefined
: 403 }); }
return jsonA JSON response. Converts `data` to JSON and sets the `Content-Type` header.
(alias) json<{
students: {
name: string;
house: string;
year: string;
}[];
}>(data: {
students: {
name: string;
house: string;
year: string;
}[];
}, init?: number | ResponseInit): Response
import json
({ students(property) students: {
name: string;
house: string;
year: string;
}[]
: await hogwarts.getStudents(method) getStudents(house: string, year: string): {
name: string;
house: string;
year: string;
}[]
(params(parameter) params: {
[key: string]: string;
}
.house, params(parameter) params: {
[key: string]: string;
}
.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(alias) interface APIEvent
import APIEvent
} from "solid-start/api"; const graphqlconst graphql: (schema: string, resolvers: any) => (event: APIEvent) => Response
= (schema(parameter) schema: string
: string, resolvers(parameter) resolvers: any
: any) => (event(parameter) event: APIEvent
: APIEvent(alias) interface APIEvent
import APIEvent
) => { return new ResponseThis Fetch API interface represents the response to a request.
var Response: new (body?: BodyInit | null | undefined, init?: ResponseInit | undefined) => Response
("GraphQL Response") }
const schemaconst schema: "
type Query {
hello: String
}
"
= ` type Query {
hello: String
}
`;
const handlerconst handler: (event: APIEvent) => Response
= graphqlconst graphql: (schema: string, resolvers: any) => (event: APIEvent) => Response
(schemaconst schema: "
type Query {
hello: String
}
"
, { Query(property) Query: {
hello: () => string;
}
: { hello(property) hello: () => string
: () => "Hello World" }
});
export const getconst get: (event: APIEvent) => Response
= handlerconst handler: (event: APIEvent) => Response
;
export const postconst post: (event: APIEvent) => Response
= handlerconst handler: (event: APIEvent) => Response
;
routes/api/graphql.ts
tsx
import { APIEvent(alias) interface APIEvent
import APIEvent
} from "solid-start/api"; const graphqlconst graphql: (schema: string, resolvers: any) => (event: APIEvent) => Response
= (schema(parameter) schema: string
: string, resolvers(parameter) resolvers: any
: any) => (event(parameter) event: APIEvent
: APIEvent(alias) interface APIEvent
import APIEvent
) => { return new ResponseThis Fetch API interface represents the response to a request.
var Response: new (body?: BodyInit | null | undefined, init?: ResponseInit | undefined) => Response
("GraphQL Response") }
const schemaconst schema: "
type Query {
hello: String
}
"
= ` type Query {
hello: String
}
`;
const handlerconst handler: (event: APIEvent) => Response
= graphqlconst graphql: (schema: string, resolvers: any) => (event: APIEvent) => Response
(schemaconst schema: "
type Query {
hello: String
}
"
, { Query(property) Query: {
hello: () => string;
}
: { hello(property) hello: () => string
: () => "Hello World" }
});
export const getconst get: (event: APIEvent) => Response
= handlerconst handler: (event: APIEvent) => Response
;
export const postconst post: (event: APIEvent) => Response
= handlerconst handler: (event: APIEvent) => Response
;
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.