Your apps talk to each other through APIs. If you work with Node.js, you need to know about HTTP request methods. It's one of the fastest ways to get better at backend work. GET, POST, PUT, PATCH, and DELETE are the five main ways to use an API. These verbs mean more than just technical things. They tell you what you want to do. They tell the server if a client is asking for data, making something new, changing an old record, or deleting it completely. If you use them correctly, your API is simple to understand, safe to use, and easy to expand. If you don't use them correctly, even a small project can get full of bugs, broken data, and unhappy customers.
This guide will show you clear, useful examples from Express.js for each HTTP method. You will learn what each method is for, when to use it, and how it works in the real world. We'll also talk about important ideas like safety and idempotency, common design mistakes that developers make, and best practices that make APIs work reliably in production. By the end, you won't just know what these HTTP verbs are; you'll also know why they exist and how to use them correctly to make REST APIs that are consistent, testable, and ready for real users.
What are the different types of HTTP requests in Node.js?
HTTP request methods define how the request and response cycle should behave when a client interacts with a resource. For teams exploring Node.js 24 features, understanding GET, POST, PUT, PATCH, and DELETE is fundamental to building reliable, scalable, and maintainable APIs with modern Node.js and frameworks like Express. They tell your API whether to get data, make something new, change information that already exists, or delete it. Think of your API as a collection of things, like users, orders, or products. Each HTTP method is a different way to interact with those resources. A GET request tells the server to send back data. A POST request tells the server to make new data. PUT and PATCH are both about updates. They can either replace a resource completely or just change part of it. DELETE makes it clear that a resource needs to be deleted.
HTTP methods have important behavioral expectations in addition to their basic purpose. One of these is security. A safe method doesn't change anything on the server. GET requests are a good example of this because they are only meant to read data. POST, PUT, PATCH, and DELETE are not safe because they change the state of the server in some way. Idempotency is another important idea that answers the question, "What happens if the same request is sent more than once?" GET, PUT, and DELETE are all idempotent, which means that if you send the same request again, the server will end up in the same state. Sending a POST request more than once usually makes more than one record, so it is not idempotent. PATCH is in the middle; it can be idempotent, but that depends on how you write your update logic. Think about resources and actions as a simple rule that ties everything together. The URL should say what you are doing, like /users/42. The HTTP method should tell you how you want to use it. For example, GET to read, PATCH to update, or DELETE to delete. If you always think this way, your Node.js APIs will be easier to understand, use, and make fewer mistakes.
Setting Up Express for Handling Requests
Before diving into HTTP methods, you need a simple but solid Express setup. This is the foundation every API call relies on. With just a few lines, you can accept JSON payloads, expose endpoints, and confirm that your service is alive and responding. The example below shows a minimal Express application with JSON parsing enabled and a health-check endpoint. This kind of endpoint is commonly used by load balancers, monitoring tools, and deployment pipelines to verify that your API is running correctly.
const express = require('express');
const app = express();
app.use(express.json());
// Health check
app.get('/health', (req, res) => res.status(200).json({ ok: true }));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`API running on port ${PORT}`));In real production systems, this is only the starting point. As your API grows, you’ll want to add schema validation to protect your data, centralized error handling to avoid duplicated logic, and structured logging to make debugging easier. Doing this early saves time, prevents subtle bugs, and keeps your API maintainable as traffic increases.
GET: Read Data Without Changing State
The only thing the GET method is good for is reading data. It is one of the most common HTTP methods in any API because it is safe, idempotent, and great for caching. In real life, GET is used to get lists, get single records, or check the status of a system. A well-made GET request should never change the state of the server; its only job is to give back information. This is a common Express example that gets a user by their ID. It takes care of the common edge case where the requested resource doesn't exist and sends back an HTTP status code that makes sense.
app.get('/users/:id', async (req, res, next) => {
try {
const user = await db.users.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.status(200).json(user);
} catch (err) {
next(err);
}
});In real life, GET requests are often used for searches with filters, paginated listings, and light readiness checks. Clients can control sorting, filtering, and pagination with query parameters without changing the endpoint itself. Adding caching headers like ETag and Cache-Control can make things work much better and lower the load on the database. If a request only reads data, GET is the right choice. This is the main rule. Your API will be easy to optimize and predictable if you keep it free of side effects.
POST: Create a New Resource
When you need to make something new, you use POST. POST changes the server's state, unlike GET, and it is not idempotent on purpose. Sending the same POST request twice usually makes two different records. This makes POST perfect for things like making new users, placing orders, or uploading files. POST requests should always include proper validation and clear responses because they change data. The request body in the example below creates a new user. The API checks that all required fields are filled out, adds the record, and sends back a 201 Created status with a Location header that points to the new resource.
app.post('/users', async (req, res, next) => {
try {
const { email, name } = req.body;
if (!email) return res.status(400).json({ error: 'Email is required' });
const user = await db.users.insert({ email, name });
res.status(201).location(`/users/${user.id}`).json(user);
} catch (err) {
next(err);
}
});In production settings, POST endpoints usually need more security. The Idempotency-Key header lets the server safely handle retries without making duplicates for important tasks like payments or order creation. This small change can stop big problems like double charges or transactions that happen over and over. The lesson is clear: Use POST when creating resources or submitting operations that require server-side processing. Return appropriate status codes and design retries carefully to prevent duplicate effects.
PUT vs. PATCH: Updates Without Confusion
Many APIs become inconsistent when data is updated. PUT and PATCH are two different ways to update things. Knowing the difference can help you avoid small bugs and losing data. PUT means to completely replace a resource. When a client sends a PUT request, it means, "This version should completely replace the current resource." Because of this, PUT is idempotent, which means that sending the same request more than once will always lead to the same final state.
Here’s an example where a profile is completely replaced using PUT:
app.put('/profiles/:id', async (req, res, next) => {
try {
const { id } = req.params;
const fullProfile = req.body; // validated full object
const updated = await db.profiles.replace(id, fullProfile);
if (!updated) return res.status(404).json({ error: 'Profile not found' });
res.status(200).json(updated);
} catch (err) {
next(err);
}
});PATCH, on the other hand, is meant for partial updates. It allows clients to send only the fields that have changed, which is especially useful for form submissions or UI-driven updates. PATCH may or may not be idempotent depending on how you implement it.
app.patch('/profiles/:id', async (req, res, next) => {
try {
const { id } = req.params;
const changes = req.body; // e.g., { displayName: 'Nia', theme: 'dark' }
const updated = await db.profiles.updatePartial(id, changes);
if (!updated) return res.status(404).json({ error: 'Profile not found' });
res.status(200).json(updated);
} catch (err) {
next(err);
}
});Both PUT and PATCH should support optimistic concurrency with ETag and If-Match headers to stop accidental overwrites in environments where multiple people are working at the same time. This makes sure that clients are always getting the most recent version of a resource and that data loss doesn't happen without anyone knowing. In short, PUT changes everything, while PATCH changes only certain fields. Both methods can support reliable updates when validation and concurrency controls are implemented correctly. However, neither PUT nor PATCH is classified as a safe HTTP method.
DELETE: Remove a Resource
DELETE is used to remove a resource from the system. Like PUT, it is idempotent—calling DELETE multiple times results in the same outcome: the resource is gone. A well-designed DELETE endpoint returns 204 No Content when the operation succeeds and 404 Not Found if the resource does not exist. This keeps responses clean and unambiguous.
app.delete('/users/:id', async (req, res, next) => {
try {
const deleted = await db.users.remove(req.params.id);
if (!deleted) return res.status(404).json({ error: 'User not found' });
res.status(204).send();
} catch (err) {
next(err);
}
});In real life, people often use DELETE to remove things from carts, cancel sessions, or make tokens invalid. Soft deletes are usually a better choice when it comes to compliance, auditing, or recovery. You don't actually delete records; instead, you mark them as deleted and keep them for future reference. One important thing to remember is to stay away from collection-wide deletes like DELETE /users. These tasks are dangerous and not often needed in production systems. The main idea is simple: DELETE should be done on purpose, be safe to try again, and be planned with long-term data responsibility in mind.
API Design Best Practices for Node.js HTTP Request Methods
Resource naming
Use nouns to represent resources, and keep collections plural, such as /users or /orders/123. This makes your API intuitive because URLs describe things, not actions. Avoid verbs in URLs like /getUsers or /createUser. The HTTP method already defines the action, so verbs in URLs only add confusion. Use nesting only when it clearly represents a relationship, for example /orders/123/items. Too much nesting makes APIs hard to read and maintain.
Use the right HTTP verb
Use GET when the intention is to read data without changing anything on the server. Clients should feel safe calling it multiple times.
Use POST when creating a new resource in a collection. Each request usually results in a new record.
Use PUT when replacing an entire resource with a new version. Clients must send the full representation every time.
Use PATCH for partial updates when only specific fields need to change. This keeps payloads smaller and updates more targeted.
Use DELETE when removing a resource permanently or logically. The intention should always be explicit and irreversible by default.
Meaningful status codes
Return 200 OK for successful reads or updates where a response body is included. This is the most common success response.
Use 201 Created when a new resource is created, along with a Location header. It tells clients exactly where the new resource lives.
Respond with 204 No Content when an operation succeeds but no response body is needed. This is ideal for deletes or silent updates.
Use 400 Bad Request when input validation fails. The client sent something incorrect.
Return 401 or 403 when authentication or authorization fails. These clearly distinguish identity issues from permission issues.
Use 404 Not Found when the requested resource does not exist. Avoid leaking internal details.
Return 409 Conflict when a duplicate or state conflict occurs. This is common in unique constraint violations.
Use 422 Un processable Entity when the data is valid JSON but semantically incorrect. It provides better feedback than a generic 400.
Idempotency
Prefer idempotent methods wherever possible to make retries safe. This is critical in unreliable network environments.
For critical POST operations like payments or order creation, support an Idempotency-Key. This prevents duplicate records when clients retry requests.
Validation and types
Validate request bodies, URL parameters, and query strings consistently. Schema tools like Joi or Zod help enforce contracts early.
Always return clear and structured error messages. Clients should know exactly what failed and why.
Versioning
Start versioning early, even if you don’t think you need it yet. URI versioning like /v1 is simple and widely used.
Choose media-type versioning if your API strategy demands flexibility. The key is consistency, not the approach itself.
Caching and performance
Use response headers such as
ETag,Last-Modified, andCache-Controlfor read-heavy endpoints to improve caching behavior and reduce unnecessary server work. This reduces server load and improves response times.Caching should be intentional and documented. Never rely on default behavior.
Concurrency control
Use ETag with If-Match headers to prevent accidental overwrites. This is essential when multiple clients update the same resource.
For PATCH operations, validate fields carefully. Partial updates can silently corrupt data if not handled properly.
Pagination and filtering
Use pagination for large datasets to avoid overwhelming clients and servers. Limit/offset or cursor-based pagination are both valid approaches.
Document default values and enforce maximum limits. This protects your API from abuse and performance issues.
Error shape
Standardize every error message and error response across the API so clients receive predictable information when a request fails. A consistent format makes error handling predictable for clients.
Example structure:
{ "error": "Message", "details": [] } Documentation
Publish an Open API or Swagger specification from the beginning. It helps with onboarding, mocking APIs, and generating typed clients. Good documentation turns a usable API into a developer-friendly one.
Common Mistakes and How to Avoid Them
When small design mistakes add up, even APIs that are meant to be helpful can become hard to use. These problems don't usually show up right away. They show up later, when clients add more users, the scale goes up, or the production traffic doesn't act as expected. If you know about these problems early on, you can avoid having to rewrite things that hurt.
Putting verbs in URLs
Endpoints like POST /createUser combine tasks and make it harder to understand APIs. The URL should show the resource, not the action. Let the HTTP method do its job. A clean endpoint like POST /users makes it clear what's going on without needing any extra explanation.
Using POST for everything
People often use POST too much because it "just works," but using it for everything breaks caching, hides intent, and makes APIs unpredictable. Each HTTP method has a specific meaning. Using the right verb makes things clearer, helps with tooling, and makes things easier to maintain over time.
Using PUT wrong for partial updates
PUT is meant to replace everything, not just one field. When used for partial updates, clients may accidentally change data they didn't mean to. PATCH is there for this exact reason and should be used when updates only affect certain fields.
Sending back 200 OK for every answer
A generic success code hides important information. Status codes are there to tell clients what to do. Using correct codes like 201 for creating and 204 for deleting makes it easier to connect APIs and cuts down on guesswork on the client side.
Ignoring timeouts and retries
Network problems happen, and a timeout doesn't always mean a request failed. Before deploying critical workflows, use API integration testing to verify timeout handling, retry behavior, and protection against duplicate operations. Apply bounded retries only when they are safe, and confirm the outcome of uncertain write operations before trying again.
Not doing validation at all
If you accept input without checking it, the data will get messed up, and errors will be hard to understand later. You should check the body, parameters, and query strings of every request. When validation fails, fail quickly and send clear, organized error messages that explain what went wrong.
Allowing destructive collection to delete
Endpoints like DELETE /users are risky and almost never needed. They make it more likely that you'll lose all your data and are hard to protect. Operations that cause damage should always be narrow and planned.
Not thinking about OPTIONS and CORS
CORS and preflight requests are two ways that modern browsers keep you safe. If OPTIONS requests aren't handled correctly or CORS headers aren't there, clients might fail before your API logic even starts.
Using identifiers that aren't stable
Using stable identifiers makes APIs easier to maintain as applications grow. UUIDs can help teams manage records consistently across services, especially when building cloud-native applications that communicate through multiple APIs.
You don't need complicated tools to avoid these problems; you just need to be consistent and respect HTTP semantics. Clients trust your API when it talks clearly, and it's much easier to find and fix problems in production.
Express.Js and Node.Js Http request methods: putting it all together
// List users with pagination
app.get('/users', async (req, res, next) => {
try {
const page = Number(req.query.page || 1);
const limit = Math.min(Number(req.query.limit || 20), 100);
const users = await db.users.list({ page, limit });
res.status(200).json({ data: users, page, limit });
} catch (err) {
next(err);
}
});
// Get one user
app.get('/users/:id', async (req, res, next) => {
try {
const user = await db.users.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.status(200).json(user);
} catch (err) {
next(err);
}
});
// Create user
app.post('/users', async (req, res, next) => {
try {
const { email, name } = req.body;
if (!email) return res.status(400).json({ error: 'Email is required' });
const user = await db.users.insert({ email, name });
res.status(201).location(`/users/${user.id}`).json(user);
} catch (err) {
next(err);
}
});
// Replace user
app.put('/users/:id', async (req, res, next) => {
try {
const replaced = await db.users.replace(req.params.id, req.body);
if (!replaced) return res.status(404).json({ error: 'User not found' });
res.status(200).json(replaced);
} catch (err) {
next(err);
}
});
// Partially update user
app.patch('/users/:id', async (req, res, next) => {
try {
const updated = await db.users.updatePartial(req.params.id, req.body);
if (!updated) return res.status(404).json({ error: 'User not found' });
res.status(200).json(updated);
} catch (err) {
next(err);
}
});
// Delete user
app.delete('/users/:id', async (req, res, next) => {
try {
const deleted = await db.users.remove(req.params.id);
if (!deleted) return res.status(404).json({ error: 'User not found' });
res.status(204).send();
} catch (err) {
next(err);
}
});Final comparison table: Node.js HTTP request methods
| Method | Purpose | Safe? | Idempotent? | Typical Path | Common Status Codes | Example Use |
|---|---|---|---|---|---|---|
| GET | Retrieve resource(s) | Yes | Yes | /users, /users/42 | 200, 304, 404 | Fetch user profile |
| POST | Create a new resource | No | No | /users | 201, 400, 409 | Register a user |
| PUT | Replace entire resource | No | Yes | /profiles/42 | 200, 201, 404, 412 | Replace profile |
| PATCH | Partial update | No | Often No (can be Yes) | /profiles/42 | 200, 204, 400, 412 | Update displayName |
| DELETE | Remove resource | No | Yes | /users/42 | 204, 404 | Delete account |
CONCLUSION
Every API you make is built on top of Node.js HTTP request methods. They set safety, idempotency, and behavior standards that your clients can count on. Your API will be easier to understand, scale, and maintain when you combine the right HTTP methods with clean resource naming, accurate status codes, proper validation, concurrency controls, and Node.js performance optimization. These five verbs are not just something you need to know for the backend; they are the building blocks of making APIs that work well, are easy to use, and will last for a long time.












