Building a Serverless Blog with SvelteKit, StepZen, and the DEV API
The code for this project is available in the StepZen-dev examples repo on GitHub
Frameworks and libraries such as React, Vue, and Svelte can all connect to GraphQL APIs to fetch the data needed to power web applications. Over the last few years there has been an influx of "metaframeworks" that provide a larger feature set such as static generation, server-side rendering, and serverless function support. Prominent examples of these metaframeworks include Next.js, Remix, RedwoodJS, and Nuxt.js.
SvelteKit is a new Svelte metaframework for building web applications with filesystem-based routing. It is inspired by Next.js and includes similar features such as server-side rendering and automatic generation of API endpoints. It is considered "serverless first" because it is designed to run on serverless infrastructure such as AWS Lambda and Cloudflare Workers.
SvelteKit's capabilities are well suited for connecting to a GraphQL endpoint built and running on StepZen because StepZen runs your queries on the server, not the client. This ensures that your API keys are protected.
This example uses the DEV API as a headless CMS to build a personal blog site. With StepZen's @rest
directive, DEV's REST API is turned into a GraphQL API. This allows the developer to query across multiple endpoints at once and removes the need to do any transformation of the resulting query response through JavaScript code.
Setup Your Project
To generate a new boilerplate SvelteKit application, use npm init svelte@next
and give your project a name.
npm init svelte@next stepzen-sveltekit-blog
Select Skeleton project
✔ Which Svelte app template? › Skeleton project
You are then asked a series of questions to configure your application. Answer based on your own preferences and use case.
For the sake of simplicity in this example, I answered no for TypeScript, ESLint, and Prettier and don't include any additional CSS libraries.
✔ Use TypeScript? … No / Yes
✔ Add ESLint for code linting? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
Create Your GraphQL Schema Files
Navigate into your project directory and create a directory for your StepZen files.
cd stepzen-sveltekit-blog
mkdir -p stepzen/schema
Create an index.graphql
file - it allows us to easily connect all of our schemas. Also create a schema file each for our user type (user.graphql
) and articles type (articles.graphql
).
touch stepzen/index.graphql \
stepzen/schema/user.graphql \
stepzen/schema/articles.graphql
For more information about how to setup your StepZen project, visit the StepZen documentation.
Configure StepZen
Create a configuration file called stepzen.config.json
. This specifies the name of our GraphQL endpoint and the location of StepZen files within the SvelteKit project. (See CLI Configuration for more information.)
echo '{"endpoint": "api/stepzen-sveltekit-blog", "root": "stepzen"}' > stepzen.config.json
Create a file called config.yaml
for holding your DEV API keys. Our config.yaml
file contains API keys to authenticate with the third-party APIs used by our project. (See Manage Configuration and Keys for more information.)
echo 'configurationset:' > stepzen/config.yaml
You can find your API keys by visiting dev.to/settings/account and scrolling to DEV Community API Keys. Include the following code in config.yaml
.
configurationset:
- configuration:
name: devto_config
devto_api_key: YOUR_KEY_HERE
Finally, make sure to add config.yaml
to your .gitignore
file:
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
config.yaml
StepZen Schema
The index.graphql
file in your StepZen project is a manifest of all the individual schema (.graphql
) files we use in the project. In this example, there is a user.graphql
file and an articles.graphql
file contained in a schema
directory, so index.graphql
looks like this:
# stepzen/index.graphql
schema
@sdl(
files: [
"schema/user.graphql"
"schema/articles.graphql"
]
) {
query: Query
}
Create User Type
The DevToUser
type includes information about authors on the DEV platform. This information is going to be displayed on the home page of our blog. You can use our JSON2SDL tool to automatically generate GraphQL types.
# stepzen/schema/user.graphql
type DevToUser {
github_username: String
id: Int
joined_at: String
location: String
name: String
profile_image: String
summary: String
twitter_username: String
type_of: String
username: String
website_url: String
}
The getUser
query returns the DevToUser
type and takes the user's name as an argument.
# stepzen/schema/user.graphql
type Query {
getUser(
id: String!, url: String
): DevToUser
@rest(endpoint: "https://dev.to/api/users/$id")
}
Deploy Your GraphQL Endpoint on StepZen
Let's test our GraphQL endpoint is deployed and running on StepZen before we start building out our frontend.
- Make sure you have the StepZen CLI installed
- Deploy your endpoint with the
stepzen start
command.
stepzen start
This deploys your endpoint and returns a production-ready endpoint to your GraphQL API. To explore this new endpoint you can use the GraphQL Explorer in the StepZen dashboard.. In this dashboard, test your endpoint with your own username in place of ajcwebdev
.
query GET_AJCWEBDEV {
getUser(id: "by_username", url: "ajcwebdev") {
github_username
id
joined_at
location
name
profile_image
summary
twitter_username
type_of
username
website_url
}
}
Building the SvelteKit Frontend
Before we can run our SvelteKit project, we need to install our dependencies.
npm i
npm run dev
npm run dev
starts your development server. Open localhost:3000 to see the project.
Now that our GraphQL API is deployed and our Svelte application is running, let's query our endpoint and display the data on the home page.
Pages
Pages are Svelte components written in .svelte
files. The filename determines the route. For example, src/routes/index.svelte
is the root of your site.
A .svelte
file contains three parts:
<script>
for JavaScript<style>
for CSS- Any markup you want to include with HTML.
<!-- src/routes/index.svelte -->
<script></script>
<section></section>
<style></style>
By default, when a user first visits the application, they are served a server-rendered version of the page in question. JavaScript is served to 'hydrate' the page and initialize a client-side router.
Navigating to other pages is handled on the client and common portions in the layout do not need to be rerendered. Pages typically generate HTML to display to the user (as well as any CSS and JavaScript needed for the page).
Endpoints
Endpoints are modules written in .js
(or .ts
) files that export functions corresponding to HTTP methods. Create an endpoint file called user.json.js
for the user query.
touch src/routes/user.json.js
Endpoints run only on the server (or when you build your site, if pre-rendering). Pages can request data from endpoints. Endpoints return JSON by default, though may also return data in other formats.
// src/routes/user.json.js
export async function post() {
const response = await fetch()
const data = await response.json()
if (data) {
return {
body: data
}
}
}
A component that defines a page or a layout can export a load
function that runs before the component is created and receive an implementation of fetch
. This endpoint and corresponding function can be used to:
- Access cookies on the server
- Make requests against the app's own endpoints without issuing HTTP calls
- Make a copy of the response and then send it embedded in the initial page load for hydration
Since endpoints only run on the server, they can be used for requests with private API keys that can't be exposed on the client. This also means you'll want to set those API keys to environment variables.
npm i -D dotenv
echo 'STEPZEN_ENDPOINT=\nSTEPZEN_API_KEY=\nDEV_TO_USERNAME=' > .env
Include your StepZen endpoint, StepZen API key, and DEV username in .env
.
STEPZEN_ENDPOINT=
STEPZEN_API_KEY=
DEV_TO_USERNAME=
Fill in the rest of the POST
request with our GraphQL getUser
query and environment variables.
// src/routes/user.json.js
import 'dotenv/config'
const { STEPZEN_ENDPOINT, STEPZEN_API_KEY, DEV_TO_USERNAME } = process.env
export async function post() {
const response = await fetch(STEPZEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `apikey ${STEPZEN_API_KEY}`
},
body: JSON.stringify({
query: `{
getUser(id: "by_username", url: "${DEV_TO_USERNAME}") {
name
summary
github_username
location
profile_image
}
}`
})
})
const data = await response.json()
if (data) {
return {
body: data
}
}
}
Load Function
load
is similar to getStaticProps
or getServerSideProps
in Next.js, except that it runs on both the server and the client. <script context="module">
is necessary because load
runs before the component is rendered. Code that is per-component instance should go into a second <script>
tag.
<!-- src/routes/index.svelte -->
<script context="module">
export const load = async ({ fetch }) => {
try {
const response = await fetch('/user.json', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
}
})
return {
props: { ...(await response.json()) }
}
} catch (error) {
console.error(`Error in load function for /: ${error}`)
}
}
</script>
<script>
export let data
</script>
Underneath the script tags, create a user component. The necessary pieces of user data can be accessed in the data.getUser
object.
<!-- src/routes/index.svelte -->
<main>
<h2>{data.getUser.name}</h2>
<h3>{data.getUser.github_username} - {data.getUser.location}</h3>
<p>{data.getUser.summary}</p>
<img src="{data.getUser.profile_image}" alt="profile pic">
</main>
Return to localhost:3000 to see the component.
Articles Schema
Now we want to be able to query for our blog posts. Include the following code in articles.graphql
.
# stepzen/schema/articles.graphql
type Article {
body_html: String
body_markdown: String
canonical_url: String
comments_count: Int
cover_image: String
description: String
path: String
public_reactions_count: Int
published_at: String
readable_publish_date: String
slug: String
tags: String
tag_list: JSON
title: String
url: String
user: DevToUser
}
type Query {
getArticleByPath(
slug: String!, username: String!
): Article
@rest(endpoint: "https://dev.to/api/articles/$username/$slug")
getArticles(
collection_id: Int
page: Int
per_page: Int
state: String
tag: String
tags: String
tags_exclude: String
top: Int
username: String
): [Article]
@rest(endpoint: "https://dev.to/api/articles")
}
This defines the types for our blog articles and queries for getting individual articles or multiple articles.
Articles Query
If you want to test your articles query against the GraphQL endpoint you deployed to StepZen, use the following query with your username included instead of ajcwebdev
:
query GET_AJCWEBDEV_ARTICLES {
getArticles(username: "ajcwebdev", per_page: 100) {
title
description
readable_publish_date
cover_image
tags
public_reactions_count
slug
url
}
}
This returns the user's first hundred blog posts.
Articles Endpoint
To query for our articles, we'll create an articles.json.js
file.
touch src/routes/articles.json.js
The articles endpoint in articles.json.js
is very similar to the user endpoint. The only difference is the GraphQL query inside the body
.
// src/routes/articles.json.js
import 'dotenv/config'
const { STEPZEN_ENDPOINT, STEPZEN_API_KEY, DEV_TO_USERNAME } = process.env
export async function post() {
const response = await fetch(STEPZEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `apikey ${STEPZEN_API_KEY}`
},
body: JSON.stringify({
query: `{
getArticles(username: "${DEV_TO_USERNAME}", per_page: 100) {
title
description
readable_publish_date
cover_image
tags
public_reactions_count
slug
url
}
}`
})
})
const data = await response.json()
if (data) {
return {
body: data
}
}
}
Articles Svelte Component
With our endpoint created, we need a Svelte component for our page. Create an articles.svelte
file.
touch src/routes/articles.svelte
The articles.svelte
component is very similar to the component in index.svelte
. It has a load
function that is fetching from articles.json
instead of user.json
.
<!-- src/routes/articles.svelte -->
<script context="module">
export const load = async ({ fetch }) => {
try {
const response = await fetch('/articles.json', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json'
}
})
return {
props: { ...(await response.json()) }
}
} catch (error) {
console.error(`Error in load function for /: ${error}`)
}
}
</script>
<script>
export let data
</script>
Since we have multiple blog posts, we need to loop over an array of articles returned from the GraphQL query. Svelte includes built-in syntax for looping over an array with an each
block.
<!-- src/routes/articles.svelte -->
<main>
{#each data.getArticles as {
title, description, readable_publish_date, cover_image, tags, slug
}}
<div>
<a target="_blank" href="https://dev.to/ajcwebdev/{slug}">
<h2>{title}</h2>
</a>
<h3>{readable_publish_date} - {tags}</h3>
<img src={cover_image} width="500" style="display: block; margin: auto;">
<p>{description}</p>
</div>
{/each}
</main>
This loops over the response from getArticles
and creates a list of blog posts that includes the title, date published, tags, and description. Each title also includes a link to the blog post. Open localhost:3000/articles to see a list of your articles.
Final User Endpoint
It would be nice to also be able to display recent blog posts on the home page. To do so, modify the main user component to include the getArticles
query and specify 5
blog posts per page.
// src/routes/user.json.js
import 'dotenv/config'
const { STEPZEN_ENDPOINT, STEPZEN_API_KEY, DEV_TO_USERNAME } = process.env
export async function post() {
const response = await fetch(STEPZEN_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `apikey ${STEPZEN_API_KEY}`
},
body: JSON.stringify({
query: `{
getUser(id: "by_username", url: "${DEV_TO_USERNAME}") {
name
summary
github_username
location
profile_image
}
getArticles(username: "${DEV_TO_USERNAME}", per_page: 5) {
title
description
readable_publish_date
slug
}
}`
})
})
const data = await response.json()
if (data) {
return {
body: data
}
}
}
Add an each
loop on the home page and create a list of article titles. Each article title has a link to the blog post.
<!-- src/routes/index.svelte -->
<main>
<h2>{data.getUser.name}</h2>
<h3>{data.getUser.github_username} - {data.getUser.location}</h3>
<p>{data.getUser.summary}</p>
<img src="{data.getUser.profile_image}" alt="profile pic">
<h3>Most Recent Articles</h3>
{#each data.getArticles as { title, slug }}
<ul>
<a target="_blank" href="https://dev.to/ajcwebdev/{slug}">
<li>{title}</li>
</a>
</ul>
{/each}
</main>
Svelte Config and Deployment Adapters
Svelte apps are built with adapters, which help optimize your project to deploy with different environments. This project uses the adapter-netlify
for Netlify.
// svelte.config.js
import adapter from '@sveltejs/adapter-netlify';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
split: false
}),
target: '#svelte'
}
};
export default config;
target
hydrates the <div id="svelte">
element in src/app.html
.
Deploy to Netlify
Create a netlify.toml
file for our build instructions.
touch netlify.toml
Set the build command to npm run build
and the publish directory to build
.
[build]
command = "npm run build"
publish = "build"
To deploy the site, push the project code to a GitHub repository and link that repository to your Netlify account. The build instructions are imported from the netlify.toml
file.
You can view a deployed example of this project at ajcwebdev-stepzen-sveltekit.netlify.app.
Conclusion
In this article, we've created a full SvelteKit project from scratch that queries a GraphQL endpoint whose backend is the DEV REST interface (deployed on StepZen). The GraphQL endpoint provides our web application with data about a user and their DEV blog posts. The project is configured for automatic deployment to Netlify and can be modified for the user's own needs.
The DEV API also provides information such as the user's organization, blog comments, and podcasts. If you want to build out this example further, grab the DEV schema in the StepZen GraphQL Studio. For more examples like this, see the StepZen-dev examples repo on GitHub.