Skip to content

Join the Seedly owners community →

API Fundamentals

Building Your First API

Creating your own API endpoint

Written by 12 min read1 activity
Pixl, your presenter

Pixl presents

Enough using other people's APIs. Build your own, with GET and POST endpoints that don't fall over on bad input.Enough using other people's APIs. Build your own, with GET and POST endpoints that don't fall over on bad input.

Pixl hammers together a small wooden service hatch with a tray on the ledge
Build your own endpoints that answer requests

You've learned how to use APIs. Now let's build one. Making an API is WAY less complicated than it sounds. In this lesson you'll build a simple API that takes requests and sends back responses.

For some perspective, I've put 1,200+ hours and $2,000+ in AI tokens into building the four Seedly products (CRM, Sites, Communities and Dispatch). Big apps get built one small piece at a time, and today's piece is tiny.

What We're Building#

We'll make a simple API for a task list. Here's what it'll do.

  • Return a list of tasks
  • Return a single task by ID
  • Create a new task

It's a small example, but it teaches the core ideas that show up in every API.

Setting Up#

For this example we'll use Next.js API routes. If you're on a different framework, the ideas are the same even if the code looks a lil different.

In Next.js, you make an API endpoint by adding a file in the app/api folder. The file path becomes the URL path.

app/api/tasks/route.js  ->  /api/tasks

Creating a GET Endpoint#

Let's start with the simplest API there is, one that returns a list of tasks.

// app/api/tasks/route.js
 
// Our fake database (in real apps, you'd use a real database)
const tasks = [
  { id: 1, title: "Learn APIs", completed: false },
  { id: 2, title: "Build a project", completed: false },
  { id: 3, title: "Deploy to production", completed: false }
];
 
export async function GET() {
  return Response.json(tasks);
}

Yep, that's the whole thing. When someone sends a GET request to /api/tasks, they get the list of tasks back as JSON.

Testing Your Endpoint#

You can test your API a few different ways.

In your browser. Just visit http://localhost:3000/api/tasks. You'll see the JSON.

With curl in the terminal.

curl http://localhost:3000/api/tasks

With a tool like Postman. Make a GET request to your URL and send it.

Getting a Single Task#

What if someone only wants one task? Then we need a dynamic route. In Next.js, you put brackets in the folder name.

app/api/tasks/[id]/route.js  ->  /api/tasks/1, /api/tasks/2, etc.
// app/api/tasks/[id]/route.js
 
const tasks = [
  { id: 1, title: "Learn APIs", completed: false },
  { id: 2, title: "Build a project", completed: false },
  { id: 3, title: "Deploy to production", completed: false }
];
 
export async function GET(request, { params }) {
  const { id } = await params;
  const task = tasks.find(t => t.id === parseInt(id));
 
  if (!task) {
    return Response.json(
      { error: "Task not found" },
      { status: 404 }
    );
  }
 
  return Response.json(task);
}

Now here's what happens.

  • GET /api/tasks/1 returns the first task
  • GET /api/tasks/99 returns a 404 error

Step 1. Get the ID from the URL#

await params gives us the ID from the URL. In Next.js 15 and newer, params is a Promise, so you have to await it before you can read anything off it (forget the await and id comes back undefined). The ID shows up as a string, so parseInt turns it into a number before we compare.

Step 2. Find the task#

We search the tasks array for a matching ID.

Step 3. Handle not found#

If nothing matches, we send back a 404 error with a message.

Step 4. Return the task#

If we find it, we send the task back as JSON.

Creating a POST Endpoint#

Now let's handle making new tasks. POST requests carry data in the body.

// app/api/tasks/route.js
 
let tasks = [
  { id: 1, title: "Learn APIs", completed: false },
  { id: 2, title: "Build a project", completed: false },
  { id: 3, title: "Deploy to production", completed: false }
];
 
export async function GET() {
  return Response.json(tasks);
}
 
export async function POST(request) {
  // Get the data from the request body
  const body = await request.json();
 
  // Validate the data
  if (!body.title) {
    return Response.json(
      { error: "Title is required" },
      { status: 400 }
    );
  }
 
  // Create the new task (next ID = highest ID so far + 1)
  const newTask = {
    id: Math.max(0, ...tasks.map(t => t.id)) + 1,
    title: body.title,
    completed: false
  };
 
  // Add it to our list
  tasks.push(newTask);
 
  // Return the new task with 201 Created status
  return Response.json(newTask, { status: 201 });
}

Now you can create tasks.

curl -X POST http://localhost:3000/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "My new task"}'

The Complete API#

Here's the full task API with GET, POST, single-task lookup and DELETE. There's one catch when you split routes across two files. Both files need to see the SAME list of tasks. So we pull the list out into its own little file and have both routes import it.

// app/api/tasks/data.js
 
// Our fake database. It lives in memory, so it resets every time the
// server restarts. In real apps, you'd use a real database.
export const tasks = [
  { id: 1, title: "Learn APIs", completed: false },
  { id: 2, title: "Build a project", completed: false }
];
// app/api/tasks/route.js
 
import { tasks } from "./data.js";
 
// GET all tasks
export async function GET() {
  return Response.json(tasks);
}
 
// POST new task
export async function POST(request) {
  const body = await request.json();
 
  if (!body.title) {
    return Response.json(
      { error: "Title is required" },
      { status: 400 }
    );
  }
 
  const newTask = {
    id: Math.max(0, ...tasks.map(t => t.id)) + 1,
    title: body.title,
    completed: false
  };
 
  tasks.push(newTask);
  return Response.json(newTask, { status: 201 });
}
// app/api/tasks/[id]/route.js
 
import { tasks } from "../data.js";
 
// GET single task
export async function GET(request, { params }) {
  const { id } = await params;
  const task = tasks.find(t => t.id === parseInt(id));
 
  if (!task) {
    return Response.json(
      { error: "Task not found" },
      { status: 404 }
    );
  }
 
  return Response.json(task);
}
 
// DELETE task
export async function DELETE(request, { params }) {
  const { id } = await params;
  const index = tasks.findIndex(t => t.id === parseInt(id));
 
  if (index === -1) {
    return Response.json(
      { error: "Task not found" },
      { status: 404 }
    );
  }
 
  tasks.splice(index, 1);
  return new Response(null, { status: 204 });
}

Best Practices#

Use the Right Status Codes#

ActionSuccess Code
GET (found)200 OK
POST (created)201 Created
PUT/PATCH (updated)200 OK
DELETE (deleted)204 No Content

Return Useful Errors#

A bare "Error." helps nobody. Tell the user what actually went wrong.

// Bad
return Response.json({ error: "Error" }, { status: 400 });
 
// Good
return Response.json(
  { error: "Title is required and must be at least 3 characters" },
  { status: 400 }
);

Validate Everything#

Check every bit of input data. Don't assume any of it is right.

if (!body.title) {
  return Response.json({ error: "Title is required" }, { status: 400 });
}
 
if (body.title.length < 3) {
  return Response.json({ error: "Title must be at least 3 characters" }, { status: 400 });
}
 
if (body.title.length > 100) {
  return Response.json({ error: "Title must be less than 100 characters" }, { status: 400 });
}

What You Built#

Your API now handles these operations.

MethodEndpointWhat It Does
GET/api/tasksGet all tasks
POST/api/tasksCreate a task
GET/api/tasks/1Get task 1
DELETE/api/tasks/1Delete task 1

That's a REAL API. It's simple, but it follows the same patterns the pros use.

TL;DR#

  • APIs are just functions that handle HTTP requests
  • Each HTTP method (GET, POST, PUT, DELETE) gets its own function
  • Use the right status codes, like 200, 201, 400 and 404
  • Always validate input data
  • Send back helpful error messages
  • In Next.js, the file path becomes the URL path

Almost There#

Pixl accepts a filled slip into a tray and slides a blank slip back with a frown
Validate input and reject requests that are missing data

You've covered the core of the API Fundamentals module. Here's everything you've picked up.

  • What APIs are and why they exist
  • HTTP methods (GET, POST, PUT, DELETE)
  • URLs and endpoints
  • JSON data format
  • Status codes
  • Authentication with API keys and tokens
  • Making API calls with fetch()
  • Receiving webhooks
  • Building your own API
  • Making smart API design decisions

These are core skills for building anything modern. Almost every app you build will use APIs, provide APIs, or both, and you're ready to work with them in real projects now. In the last lesson of this module, you'll check out two real APIs you're likely to run into...

This lesson ends with a short activity.