Building a simple API with Next.js
Why?
I had been watching Theo (opens in a new tab) on YouTube rave about T3 (opens in a new tab), Next.js (opens in a new tab), and all kinds of other web dev things. I wasn’t currently and didn’t want to be making a web app, so this was just interesting commentary on stuff I knew nothing about. Having backend and frontend with type safety in the same codebase sounded like a dream, but not one that was applicable to me, a maker of native Swift apps.
But, I've been intrigued with the idea of creating my own APIs for Shorts or Pants for a while, especially for some upcoming features. I had been using Firebase and Firestore kinda by default (they’re free and easy to set up), but the path to writing your own APIs on that stack always seemed indirect and odd. Also, I was very worried about migrations using amorphous JSON blobs to store everything. At the same time, spinning up my own stack with a DB, proper backend, queues, k8s, etc, seemed like total overkill for what I was doing.
So when Theo raved about Vercel (opens in a new tab) and PlanetScale (opens in a new tab), my interest was properly piqued. Vercel deployment seemed stupid simple and crazy fast, and PlanetScale seemed to solve the migration-related problems with DBs that I had run into countless times in other projects and jobs.
But, I didn't need a DB quite yet, and I really just wanted to see how hard it would be to get the simplest possible API working with Next.js and Vercel.
Making an API
So out of curiosity, I went to Vercel, added a new project with the Next.js template, connected my GitHub account, and had a deployment of a new web app up in literally minutes. This kinda blew my mind (I had no idea it could be that easy).
But, I wanted to start a project myself (not just blindly using the Vercel template), so I went to the docs for Create Next App (opens in a new tab). Again, super quickly, I had myself a working app. I didn’t use Create T3 App (opens in a new tab) because I didn’t see any need for a frontend quite yet. I was here to test a minimal API example, so I went to the Next.js docs for API Routes (opens in a new tab) and modified the initial hello.ts
example to try some stuff.
Fetching data from an external API
First, I wanted to try just passing API requests through to the weather API I use (OpenWeather (opens in a new tab)), returning their responses, and not yet worrying about params, error handling, rate limiting, auth, and all the other shenanigans you’d really need. And funny enough, doing this accomplishes both of the use cases highlighted by the docs:
- Masking the URL of an external service (e.g.
/api/secret
instead ofhttps://company.com/secret-url
)- Using Environment Variables (opens in a new tab) on the server to securely access external services.
It took me a minute to figure out whether I should use next-fetch (opens in a new tab) or just fetch (opens in a new tab), but I decided to just go with fetch to get going. In a few lines of code, I had that working. Again, I was super impressed. This was the effort I thought it should take to get an incredibly basic API going, but I had never found a stack that lived up to that. Here’s what I ended up with (again, this is only a starting point, and definitely not ready for production):
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const appID = process.env.OWM_APP_ID;
const owmResponse = await fetch(`https://api.openweathermap.org/data/2.5/onecall?appid=${appID}&lat=38.5449&lon=-121.7405&exclude=minutely&units=metric`, { cache: 'no-store' });
const owmResponseJson = await owmResponse.json();
res.status(200).json(owmResponseJson);
}
// Example usage: GET yourapp.vercel.app/api/weather
Passing params
Next up was to actually add inputs to this API (so to make it no longer useless). I went with query params as a starting point, rather than using a json body, since that’s what the OWM API uses. Grabbing params was easy enough, as was passing them into the URL. Here’s the result, which passes all the params through to the weather API, and returns the response:
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const params = req.query;
const { lat, lon, exclude, units } = params;
//im guessing we can build this url in a smarter way, but trying this first...
const appID = process.env.OWM_APP_ID;
const owmResponse = await fetch(
`https://api.openweathermap.org/data/2.5/onecall?appid=${appID}&lat=${lat}&lon=${lon}&exclude=${exclude}&units=${units}`,
{ cache: 'no-store' }
);
const owmResponseJson = await owmResponse.json();
res.status(200).json(owmResponseJson);
}
// Example usage: GET yourapp.vercel.app/api/weather?lat=38.5449&lon=-121.7405&exclude=minutely&units=metric
Not ready for production...
This example was just enough to start talking to a server, but as mentioned a few times, it doesn't have what it needs to go live quite yet.
Some things you'd want to consider before making this live
- It likely needs authentication (assuming only logged in users should be able to fetch weather)
- Rate limiting will help prevent individuals from repeatedly calling this API and blowing up your costs
- We should check the method (opens in a new tab) of the request to make sure it's actually a real API route
- Incoming parameters need to be validated, and an error should be returned if they are invalid. Some examples of what could be invalid:
- Required parameters could be missing or null
- Parameters could be the wrong type
- Parameters could have invalid values (like passing in 1000 for latitude)
- Speaking of errors, we need error handling. This is a huge topic and takes longer to handle than anyone would like it to.
- Decide on your caching strategy.
- This example avoids Next.js' built-in caching (via
cache: 'no-store'
) in order to always get fresh weather data. - In reality, you'd still want to do some caching since the weather data update interval is not very fine (for OWM, it's between ~10 minutes and ~2 hours (opens in a new tab)).
- For data that doesn't change rapidly or doesn't need to be super fresh, caching is a must.
- This example avoids Next.js' built-in caching (via
But it works!
With that, I have the ability to deploy to Vercel by committing and pushing to main. I also have secrets stored safely in the server, and isolation of external API calls to the backend. With this approach, response time will definitely be increased since two API calls are being made instead of one, but the benefits heavily outweigh that, and I haven’t even explored optimizing that by using the Edge (opens in a new tab). Also, isolating all weather API calls to my own API would allow me to do things like cache requests from similar locations (in a dense city for example) to reduce the number of calls I have to make to OWM.
So, I can make an API! Yeah there’s a lot more to do, and it’s not the priority right now, but it was a great learning experience. This is the kind of goal-free exploration you can’t usually justify prioritizing while working at a company (too many tickets to do, PRs to review, docs to write, etc.), but I was super happy to do it.
Get Shorts or Pants for iPhone and iPad (opens in a new tab)
Sign up for the newsletter (opens in a new tab)
Thanks for reading!